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 a modern browser.
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.
How would you write hello world in javascript? I might write console.log("Hello World!");
Can you write a test that helps you implement it. Sure. I will put it in test/greeter-test.js
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
Can you make it fail?
var greeter = {
greet: function () {
}
};
Is the message clear? Yes
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?
How does writing to console.log
feel? Ugly! I am going to rewrite it so I don't have to care. Notice I create a constructor for greeter
that takes a voice
.
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)
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.
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.
How do you run it? I add the following inside a document ready block inside of greeter.js
and check the browser and browser console (F12).
$( document ).ready( 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.
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("wazzaap");
assert.calledWith(console.log, "wazzaap");
}
});
Does it pass? No, it outputs an error.
Can you make it fail? Sure
var voice = function (speech) {
};
Is the message clear? Yes
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.
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("wazzaap");
assert.calledWith(jQuery.prototype.html, "wazzaap");
}
});
Does it pass? No, it is failing. Notice I didn't forget my comma :)
Is the message clear? Yes.
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.
buster.testCase("Voice", {
"speaks to the DOM": function() {
this.stub(jQuery.prototype, "html");
voice('wazzaap');
assert.calledWith(jQuery.prototype.html, 'wazzaap');
}
});
var voice = function (speech) {
$("#voiceBox").html(speech);
};
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);
}
});
Can you make this one pass? I just need to call show.
var voice = function (speech) {
$("#voiceBox").html(speech);
$("#voiceBox").show();
};
I see some duplication. I am cleaning that up right now.
var voice = function (speech) {
var voiceBox = $("#voiceBox")
voiceBox.html(speech);
voiceBox.show();
};
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");
...
}
});
That's better, but the message hangs around forever. Can you make it go away? Yes, I am adding a test for that. Notice how I use a Fake timer to avoid slow tests.
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);
}
});
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);
};
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/);
}
});
var CreateGreeter = function (voice) {
...
greet: function (name) {
voice("Hello, " + name + "!");
}
...
};
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.
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. Also notice, I added the ear
argument.
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.
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.
var ear = function (callback) {
callback("Martin");
};
What is up with 'Martin'? Just a place holder, it will be gone soon.
buster.testCase("Ear", {
...
,
"shows the earBox": function () {
this.stub(jQuery.prototype, "show");
ear(this.stub());
assert.called(jQuery.prototype.show);
}
});
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");
...
});
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);
}
});
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? Yes. To fix it, I need to fire a submit
event and I don't know how to do that. I will let the acceptance tests cover it for now. In the mean time, I will skip this test.
buster.testCase("Ear", {
"//notifies after hearing something": function(done) {
...
});
Does the end to end test pass? Not yet. I need to update the document.ready function.
$( document ).ready (function () {
CreateGreeter(voice, ear).listen()
});
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; Todo this should be in ear
},
...
,
pontificate: function () {
voice(this.name);
}
};
};
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. Notice I remembered to add guru
the parameters.
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. I need to replace guru
and the assert
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/);
},
...
});
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. I can fix this with a guru
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.
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);
},
...
,
"prompts guru to pontificates every few seconds": function () {
var pontificator = this.stub(this.greeter, "pontificate");
this.greeter.greet("Michael");
assert.called(this.repeater);
}
});
var CreateGreeter = function (voice, ear, guru, repeater) {
return {
greet: function (name) {
...
repeater(this.pontificate.bind(this), 7000);
...
});
var repeater = function (callback, timeout) {
setInterval(callback, timeout);
};
...
$( document ).ready( function() {
CreateGreeter(voice, ear, guru, repeater).listen();
});
You made it, good work. You deserve a pat on the back, a snack and a a bit of wisdom. Thank you!
mockist-style-javascript-tdd-kata by Zhon Johansen, David Adsit is licensed under a Creative Commons Attribution 3.0 Unported License.