Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Kata for learning mockist/interaction/London style TDD in Javascript
JavaScript
branch: master

This branch is 10 commits behind zhon:master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
lib
test
vendor
README.md
greeter-test.html

README.md

Introduction

I hear you want to learn mockist/interaction/London style test driven development? I do! I do!

What app do you want to build? A wisdom sharing greeting bot.

How are you going to end to end test it? I think manually testing it will be sufficient.

Why not automated your end to end test? Oh, I will just not in this kata.

Where are the unit tests and code on the file system? The code is structured as a standard node.js application:

  • Tests are in test/greeter-test.js
  • Application code is in lib/greeter.js

What is buster? buster.js is the javascript testing framework I am using for this kata.

What is wisdom.js? That is the man behind the curtain.

How do you run the tests? Open greeter-test.html in Chrome or Firefox (not IE).

How do you run the application? Open greeter-test.html. A build script strips out all the testing for production. I won't show you that in this kata.

Starting

How would you write hello world in javascript?

console.log("Hello World!");

Test 1

Shouldn't you have written a test first? Oops, I forgot this was a TDD kata.

buster.testCase("Greeter", {

    "calls console log with hello": function () {
        this.stub(console, "log");
        greeter.greet();
        assert.called(console.log);
        assert.match(console.log.firstCall.args[0], /Hello/);
    }

});

Is this test failing with an Error or a Failure? Error! I get ReferenceError: greeter is not defined

Test 1 - failing

Can you make it fail?

var greeter = {
    greet: function () {
    }
};

Is the message clear? Yes

Test 1 - passing

How do you make it pass?

    greet: function () {
        console.log("Hello, world!");
    }

Is your test passing? Yes

Do you like this implementation? No

Why not? I am logging to the console. Nobody logs to the console.

Where should your output be going? I don't know. HTML, stderr, stdout, audio?

Test 1 - refactor

Can you rewrite your code and test without specifying where the output will go? Sure

Ok, rewrite the first test.

buster.testCase("Greeter", {

    "uses voice to greet": function() {
        var voice = this.stub();
        var greeter = CreateGreeter(voice);
        greeter.greet();
        assert.called(voice);
        assert.match(voice.firstCall.args[0], /Hello/);
    }

});

Is this test erroring or failing? Erroring

Is 'erroring' a word? I don't know, but erring is a word.

How do you get a failure? Implement CreateGreeter(voice)

Test 1 - failing (again)

var CreateGreeter = function (voice) {
    return {
        greet: function () {
        }
    };
};

Why didn't you finish the implementation? I wanted to see the test fail and check it's failure message.

Test 1 - passing(again)

Is it difficult to make it pass now? No

        greet: function () {
            voice("Hello, world!");
        }

Are you done? No

Why not? It doesn't do anything!

How do you know it doesn't do anything? If I run it, it shows nothing.

End to End Test

How do you run it? I add the following to the bottom of greeter.js and check the browser and browser console (F12).

$(function () {
    CreateGreeter(voice).greet();
});

You are using the test HTML as your final HTML? Yes, I will run it through a build system which will strip out the testing.

For simple things, is manually running an end to end test sufficient? Yes

Ok, run it! Like I said, it didn't show anything (it throws a ReferenceError).

How do you make it do something? Implement voice.

How do you implement voice? I don't know. Where should I send the output?

If I pick console.log, can you implement it? Of course.

Test 2 (new test case)

Should you write a test first? Yes, this is a TDD Kata after all.

buster.testCase("Voice", {

    "speaks via console.log": function () {
        this.stub(console, "log");
        voice("sup");
        assert.calledWith(console.log, "sup");
    }

});

Does it pass? No, it outputs an error.

Test 2 - failing

Can you make it fail? Sure

var voice = function (speech) {
};

Is the message clear? Yes

Test 2 - passing

Can you make it pass? No problem.

var voice = function (speech) {
    console.log(speech);
};

It everything working? Yes

How do you know it works? I run all my tests including a manual end to end test. The console shows 'Hello, world!'.

Are you happy with this implementation? Not quite. Console.log is not viable javascript output.

Where do you want the output to go? A web page.

Do you want to manipulate the DOM directly? Sure, why not?

Did you forget about IE6 and IE7? {shudder} I better use jQuery instead.

Test 3

Now that you have a DOM element for your output, will you change the test? Obviously

buster.testCase("Voice", {
    ...

    ,
    "speaks to the DOM": function () {
        this.stub(jQuery.prototype, "html");
        voice("sup");
        assert.calledWith(jQuery.prototype.html, "sup");
    }

});

Does it pass? No, it is failing. Notice I didn't forget my comma :)

Is the message clear? Yes.

Test 3 - passing

Can you make it pass? Easily.

var voice = function (speech) {
    console.log(speech);
    $("#voiceBox").html(speech);
};

Hey! you're still logging to the console! I am removing the test and the code for console logging now.

Test 3 - clean up

buster.testCase("Voice", {

    "speaks to the DOM": function() {
        this.stub(jQuery.prototype, "html");
        voice('sup');
        assert.calledWith(jQuery.prototype.html, 'sup');
    }

});
var voice = function (speech) {
    $("#voiceBox").html(speech);
};

Test 4

I just ran an end to end test and I don't see any output. What gives? The voiceBox div is hidden. I will show it now.

buster.testCase("Voice", {
    ...

    ,
    "shows the voice box": function () {
        this.stub(jQuery.prototype, "show");
        voice("howdy, y'all");
        assert.called(jQuery.prototype.show);
    }

});

Test 4 - passing

Can you make this one pass? I just need to call show.

var voice = function (speech) {
    $("#voiceBox").html(speech);
    $("#voiceBox").show();
};

Test 4 - refactor

I see some duplication. I am cleaning that up right now.

var voice = function (speech) {
    var voiceBox = $("#voiceBox")
    voiceBox.html(speech);
    voiceBox.show();
};

End to End Test

Why am I seeing the wrong message? If I remove the testing lines from the html file, you will see the correct message (Hello World). That reminds me, I need to stub html() and show() in all Voice tests.

buster.testCase("Voice", {
    "speaks to the DOM": function() {
        ...
        this.stub(jQuery.prototype, "show");

    ...

    ,
    "shows the voice box": function() {
        this.stub(jQuery.prototype, "html");
        ...
    }

});

Test 5

That's better, but the message hangs around forever. Can you make it go away? Yes, I am adding a test for that.

buster.testCase("Voice", {
    ...

    ,
    "hides the voice box after a few seconds": function () {
        this.stub(jQuery.prototype, "html");
        this.stub(jQuery.prototype, "show");
        this.stub(jQuery.prototype, "slideUp");
        var clock = this.useFakeTimers();

        voice("goodbye");
        refute.called(jQuery.prototype.slideUp);
        clock.tick(5000)

        assert.called(jQuery.prototype.slideUp);
    }

});

Test 5 - passing

I noticed that you used "refute.called" Yes, that ensures that I don't just call slideUp immediately.

Oh, you will be calling setTimeout? Yes

var voice = function (speech) {
    ...
    setTimeout(function () { voiceBox.slideUp() }, 5000);
};

Test 6

The greeting feels a little impersonal. I am adding the greetee's name.

buster.testCase("Greeter", {
    ...

    ,
    "greets a person": function () {
        var voice = this.stub();
        var greeter = CreateGreeter(voice);
        greeter.greet("Ward");
        assert.called(voice);
        assert.match(voice.firstCall.args[0], /Ward/);
    }

});

Test 6 - passing

var CreateGreeter = function (voice) {
    ...
        greet: function (name) {
            voice("Hello, " + name + "!");
    ...
};

Test 6 - refactor

The tests have duplicate setup. I am extracting it into a setUp.

buster.testCase("Greeter", {

    setUp: function() {
        this.voice = this.stub();
        this.greeter = CreateGreeter(this.voice);
    },
    "uses voice to greet": function() {
        this.greeter.greet();
        assert.called(this.voice);
        assert.match(this.voice.firstCall.args[0], /Hello/);
    },
    "greets a person": function() {
        this.greeter.greet("Bob");
        assert.called(this.voice);
        assert.match(this.voice.firstCall.args[0], /Bob/);
    }

});

I notice the End to End test shows 'Hello, undefined!'. Yes, I am fixing it by listening for a name.

Test 7

buster.testCase("Greeter", {
    setUp: function() {
        ...
        this.ear = this.stub();
        this.greeter = CreateGreeter(this.voice, this.ear);
    },
    ...

    ,
    "listens with an ear": function () {
        this.greeter.listen();
        assert.called(this.ear);
   }

});

I like how you used setUp. Yeah. I am glad we removed that duplication so that we can update all the tests at once.

Test 7 - passing

var CreateGreeter = function (voice, ear) {
    return {
        ...

        ,
        listen: function () {
            var self = this;
            ear(function (name) {
                self.greet(name);
            });
        }
    };
});

You seem to be missing an ear. Uh, I can hear you! Yes, I am writing a test for that.

Test 8 (new test case)

buster.testCase("Ear", {

    "notifies after hearing something": function (done) {
        var callback = this.spy(function (sound) {
            assert.equals(typeof sound, "string");
            done();
        });
        ear(callback);
        assert.called(callback);
    }

});

Why are you using a spy? Spys allow asserts without changing the behavior of function it is spying on.

Test 8 - passing

var ear = function (callback) {
    callback("Martin");
};

What is up with 'Martin'? Just a place holder, it will be gone soon.

Test 9

buster.testCase("Ear", {
    ...

    ,
    "shows the earBox": function () {
        this.stub(jQuery.prototype, "show");
        ear(this.stub());
        assert.called(jQuery.prototype.show);
    }

});

Test 9 - passing

var ear = function (callback) {
    var earBox = $("#earBox")
    earBox.show();
    ...
};

Why is the earBox showing up? I am stubbing 'jQuery.show' in all the Ear tests to make it disappear.

buster.testCase("Ear", {

    "notifies after hearing something": function(done) {
        this.stub(jQuery.prototype, "show");

    ...

});

Test 10

buster.testCase("Ear", {
    ...

    ,
    "watches for a submit event": function () {
        this.stub(jQuery.prototype, "show");
        this.stub(jQuery.prototype, "submit");
        ear(this.stub());
        assert.called(jQuery.prototype.submit);
    }

});

Test 10 - passing

var ear = function (callback) {
    var earBox = $("#earBox")
    earBox.show();
    $("form").submit(function () {
        callback($("#input").val());
        earBox.hide();
        return false;
    });
};

Thanks for getting rid of 'Martin'; you noticed a different test suddenly failed? I know, I should fix it by splitting the function. I just don't want to do it. I will defer it.

buster.testCase("Ear", {

    "//notifies after hearing something": function(done) {
    ...
});

End to End

Does the end to end test pass? Not yet. I need to update the document.ready function.

$(function () {
    CreateGreeter(voice, ear).listen()
});

Test 11

What now? I seek wisdom.

buster.testCase("Greeter", {
    ...

    ,
    "sends a pearl of wisdom": function () {
        this.greeter.greet("Kent");
        this.greeter.pontificate();
        assert.calledTwice(this.voice);
        assert.match(this.voice.secondCall.args[0], /Kent/);
    }

});

You didn't check the wisdom? I haven't been enlightened. I just want to pass tests.

var CreateGreeter = function (voice, ear) {
    return {
        greet: function (name) {
            ...
            this.name = name;
        },
        ...
        ,
        pontificate: function () {
            voice(this.name);
        }
    };
};

Test 12

Were do you find wisdom? A guru?

buster.testCase("Greeter", {

    setUp: function() {
        ...
        this.guru = this.stub();
        this.greeter = CreateGreeter(this.voice, this.ear, this.guru);
    },
    ...

    ,
    "pontificates guru wisdom": function () {
        this.greeter.pontificate();
        assert.called(this.guru);
    }

});

How are you going to make this pass? A guru with a callback.

var CreateGreeter = function (voice, ear, guru) {
    return {

        ...

        ,
        pontificate: function () {
            var self = this;
            guru(function (wisdom) {
                voice(self.name + ', ' + wisdom);
            });
        }
    };
};

Another one of your tests failed. That is part of designing. Here is a fix.

buster.testCase("Greeter", {

    setUp: function () {
        ...
        this.guru = this.spy(function (callback) { callback('nugget of wisdom'); });

    ...
    "sends a pearl of wisdom": function () {
        ...
        assert.match(this.voice.secondCall.args[0], /Kent.*wisdom/);
    },
    ...
});

Test 13 (new test case)

You seem a little distracted? Yes, I really need a guru.

buster.testCase("Guru", {

    "shares wisdom in a callback": function (done) {
        guru(function (wisdom) {
            assert.equals(typeof wisdom, "string");
            done();
        });
    }

});

Is the test erroring or failing? ReferenceError

Test 13 - timing out

var guru = function (callback) {

};

Is the test erroring or failing? Neither, it is timing out.

How do you fix it? guru calls the callback.

var guru = function (callback) {
    if (typeof wisdom.index === 'undefined' || wisdom.index === wisdom.length) {
        wisdom.index = 0;
    }

    callback(wisdom[wisdom.index++]);
};

What is wisdom.index? You didn't test it. A magical way of getting wisdom. Normally I would test it, but this kata is long enough.

Test 14

How are we going to finish? With pontificate repeating different sayings.

buster.testCase("Greeter", {
    setUp: function() {
        ...
        this.repeater = this.stub();
        this.greeter = CreateGreeter(this.voice, this.ear, this.guru, this.repeater);
    },

    ...

    ,
    "starts the guru speaking every few seconds": function () {
        var pontificator = this.stub(this.greeter, "pontificate");
        this.greeter.greet("Michael");
        assert.called(this.repeater);
    }
});

Test 14 - passing

var CreateGreeter = function (voice, ear, guru, repeater) {
    return {
        greet: function (name) {
            ...
            repeater(this.pontificate.bind(this), 7000);
    ...
});

End to End Working

var repeater = function (callback, timeout) {
    setInterval(callback, timeout);
};

...

$(function () {
    CreateGreeter(voice, ear, guru, repeater).listen();
});

If you made it here, you deserve a pat on the back and a snack. Thank you!

Something went wrong with that request. Please try again.