Mobile App Front End Behaviour Driven Development (BDD) with Jasmine and Karma


In this video we are going to take a look at how TweetHours is progressing from the front-end or mobile app perspective, and cover how the code is being written using Jasmine tests (v2.3.4 to be exact) to enable the code to be written in a Behaviour Driven Development style. I should also point out - for those who are as interested in Testing as I am - that on the back-end Symfony side of things, the tests are being written using Behat 3 and PhpSpec, so BDD all round.

In this video I am covering the section of the JavaScript code that takes in an array of TweetHours in the form:

[
    { id: 3, title: 'event3', day: 'Monday', start: "12:00", duration: 1 },
    { id: 4, title: 'event4', day: 'Monday', start: "12:00", duration: 2 },
    { id: 5, title: 'event5', day: 'Monday', start: "13:00", duration: 1 },
];

And should make the following tests go green:

describe('nextStartDate test', function() {

    it('should set timestamp to today if day is today and time has not yet been', function(){
        var todayAt1130 = moment("2015-01-01 11:30", "YYYY-MM-DD H:mm").format('x');
        expect(DateProcessing.nextStartDate({day: 'Thursday', start: "11:30" })).toEqual(todayAt1130);
    });

    it('should set timestamp to next week if day is today and time has already passed', function(){
        var nextThursdayAt1100 = moment("2015-01-08 11:00", "YYYY-MM-DD H:mm").format('x');
        expect(DateProcessing.nextStartDate({day: 'Thursday', start: "11:00"})).toEqual(nextThursdayAt1100);
    });

    it('should set the timestamp to tomorrow if the day is tomorrow', function() {
        var tomorrowAt0300 = moment("2015-01-02 03:00", "YYYY-MM-DD H:mm").format('x');
        expect(DateProcessing.nextStartDate({day: 'Friday', start: "03:00"})).toEqual(tomorrowAt0300);
    });

    it('should set the timestamp to next Wednesday if the day is yesterday', function() {
        var nextWednesday = moment("2015-01-07 07:00", "YYYY-MM-DD H:mm").format('x');
        expect(DateProcessing.nextStartDate({day: 'Wednesday', start: "07:00"})).toEqual(nextWednesday);
    });

});

I should also mention that I am making heavy use of the excellent moment.js library in this project.

The idea behind this code is such that we need to determine when the TweetHour is due to start. The tricky part is that all we have to on is the day name and the start time. More commonly when dealing with dates and times you would have specific dates - January 3rd 2015 at 14:25 for example - but in this app, the hours are recurring every week, so the date itself is always changing.

As such, the code should be able to determine that if today is January 1st, 2015 (which just so happens to be a Thursday, if your memory / Googling skills are up to the task) and we pass in an object representing a TweetHour that has a start of 'Thursday', then we need to determine if the next instance of this hour should be today or next Thursday.

Testing With Dates and Times

There is another issue which is one of those classic development hurdles you don't anticipate when telling your client / project manager / naive self, and that is that testing with Dates and Times isn't quite as straightforward as you might think.

Let's take a quick example.

We write a test that determines that if today is Monday, and we have a TweetHour starting today at 1pm, then assuming now is 11am, the timestamp we get back should be for this afternoon.

Somewhere inside our test suite we add in:

var date = moment();

We write the test, it passes, we move on.

After a light lunch of last nights takeaway pizza and a can of coke, we find ourselves back at our desk just after 1pm. Before moving on to the next feature, we do a quick run of our test suite. What's this? Suddenly our test is failing.

Alas, we relied on our date being correct as of when we wrote the test.

Ok, that's not the most extravagant example, but honestly, it's quite easy to write tests that rely on a date which might not behave as we expect. This will typically manifest as a test that sometimes passes, and other times, fails. Absurd. Preposterous. Unthinkable!

Instead, we should let Jasmine create a mocked date instance for us, which will trick our real code into behaving as though our tests always ran at that specific time.

We can then declare this somewhere common, e.g.:

'use strict';

describe('Service: DateProcessing', function () {

    var DateProcessing;

    beforeEach(inject(function(_DateProcessing_) {
        jasmine.clock().mockDate(new Date(moment("2015-01-01 11:00", "YYYY-MM-DD H:mm"))); // Thursday, Jan 1st 2015 11:00
        DateProcessing = _DateProcessing_;
    }));

    // * snip *

Bonza, we have our system under test in a known state.

Expect The Expected

With our time set in stone, writing the logic is pretty straightforward.

Essentially we want to pass in a day and a time. If that day and time occurs in the past (in the scope of the current week), then we want to get back a timestamp for that day at that time next week.

If the day and time occurs in this week and is yet to happen - lets say it's Wednesday at 3pm, but our TweetHour happens at Saturday at 6pm - we want a timestamp from this week.

With our tests written, all we need to do is make them go to green and once they do, we know we can rely on this section of code to behave as expected.

Ultimately we end up with quite a concise block of code that doesn't do a whole lot, but what it does do is known to work:

        this.nextStartDate = function(tweetHourDetails) {
            var hours = tweetHourDetails.start.split(':')[0];
            var minutes = tweetHourDetails.start.split(':')[1];
            var today = moment(0, "HH");
            var tweetHourDate = today.day(tweetHourDetails.day).hours(hours).minutes(minutes).seconds(0);

            if (tweetHourDate <= moment()) {
                tweetHourDate.add(7, 'days');
            }

            return tweetHourDate.format('x');
        }

Regardless of whether this is PHP, JavaScript, Python, or any other language, testing as a concept is the same. We write some failing tests, then we write just enough code to make those tests pass. After that, if possible, we clean up our code by refactoring to a more concise implementation. Rinse and repeat until the application does everything you as a developer need it to do.

The nice thing about writing code this way is that we get built in error checking for ourselves and anyone we are sharing our code with. Others, or even ourselves, can read the tests - especially BDD style tests like Behat and Jasmine tests - and understand what the code is doing, and what to expect if the code is given specific values.

Another way to put this is that the tests become living documentation. No one likes writing documentation. At least, no one I have ever met. As long as we keep our tests clean, use good method names for both our methods themselves and our test descriptions (note, the video proves I made a boo-boo on this front), we can avoid having to keep a second, separate set of documentation. And that makes me a much happier developer. Hopefully you feel the same way too.

Episodes