In Agile development, developers write tests before implementing a feature. Unit tests should already be in your ADN, so let's talk about functional tests instead. Functional tests are the technical translation of the acceptance tests written by the customer at the back of a user story card. Let's see how to achieve functional testing for Node.js applications.
The customer wants a contact page. She wrote the following User Story:
As a user,
Given I found the contact page,
I want to fill in my contact information and message,
and have it sent to the site owner.
Here is the mockup associated with the story:
While discussing this story with the customer during a planning poker, the team lists the acceptance tests that the application should pass to validate the user story:
You could rush to implement the contact page requirements, and then check the acceptance tests by hand with a web browser. But that wouldn't be very agile.
In test-driven development, you should write tests first. Let's bootstrap a file called
If this looks exactly like the acceptance tests written above, that's for a reason. This first step could, in theory, be achieved directly by the Product Owner, because it requires no knowledge of anything technical.
Next, run the tests. That's right, even before you write the first line of the actual contact page, you should run the tests. This tutorial uses the
mocha test framework, so either add it to your
package.json in the
devDependencies section, or better, install it globally once and for all:
$ npm install -g mocha
Now you can run the new (empty) functional test:
$ mocha test/functional/contact.js ...... ✔ 6 tests complete (2ms) • 6 tests pending
So far, so good: all the tests are pending.
Before implementing the functional tests, let's switch to another way to output mocha tests results: the "spec" reporter. To do so, either add the
--reporter spec option when calling the
mocha command, or even better, add it to the
test/mocha.opts file and you'll never have to add it again. And since you're adding command line options for mocha, opt for the
describe() statements under the whole
test/ folder hierarchy.
--reporter spec --recursive
Now running the test is easier (no need to specify the test to run) and takes a whole new dimension:
$ mocha contact page - should show a contact form - should refuse empty submissions - should refuse partial submissions - should keep values on partial submissions - should refuse invalid emails - should accept complete submissions ✔ 6 tests complete (2ms) • 6 tests pending
The "spec" reporter takes advantage of the
it() descriptions to display a nicely formatted list of application requirements. That way, you can even let the Product owner look at the test results, and the tests document the code.
Functional tests should browse a special version of the web application - the "test" application. This version should use test data, test configuration, and should not interfere with the development or production versions. A good practice is to setup test data and start a new instance of the application inside each functional test. The ideal place to put the related code in is the
before() function, that mocha executes... before the tests. And of course, don't forget to close the application test instance when the test ends.
before() function starts the server on a custom port, to avoid side effects on the development version of the application. In order to allow a Node.js server script to be started by functional tests, it should be exported as a module, and start (or "listen") only when called directly. This is how the main
server.js file should end:
Tip: If you want to use different configuration values for the test environment, you should use an environment-aware configuration utility. I recommend the excellent config module for that purpose.
zombie to the
Then install it (with
npm install) and edit the contact functional test file again:
You can add as many
before() calls as you wish in a functional test; mocha executes them in series. The second
before() call is asynchronous - you can tell from the
done callback passed as parameter to the
before() argument function. In this function, the browser's
visit() method loads the contact page, waits for the page to fully load and process events, and then calls the
done callback function - allowing mocha to start actual tests.
A functional test is a simple callback function. Mocha considers the test as valid if the function doesn't throw any error. You can either test assertions manually and throw errors upon unexpected result, or use an assertion module. Node.js comes with the
assert module, which is more than enough for the contact page functional tests.
These three assertions are pretty basic. They check that the page returns an HTTP code 200, that it's actually the contact page, and that it contains a form with several fields labeled as in the mockup. The Zombie browser
text() function takes a CSS selector as argument, and returns the text of the matching element(s) in the DOM. It's a very straightforward way to implement functional tests assertions.
Now that the test suite has one real test, it's time to run it:
$ mocha contact page 1) should show a contact form - should refuse empty submissions - should refuse partial submissions - should keep values on partial submissions - should refuse invalid emails - should accept complete submissions ✖ 1 of 6 tests failed: 1) contact page should show a contact form: Error:...
The test fails, which is normal. The page content doesn't exist yet. You should always check that a test fails if the implementation is wrong, and running a test before actually starting the implementation is the best way.
The implementation of the contact form is left as an exercise to the reader. Depending on the framework and templating engine you use, this implementation may vary a lot. But you have one obligation: it must pass the test. That means that once the implementation is finished, running the test should yield the following result:
$ mocha contact page ✓ should show a contact form - should refuse empty submissions - should refuse partial submissions - should keep values on partial submissions - should refuse invalid emails - should accept complete submissions 6 tests complete (20 ms) 5 tests pending
The second test should check that an empty form submission displays the form again with an error. Whether you choose to implement this client-side or server-side (you should do both), this implies interacting with the contact form. The Zombie browser provides a full-featured API to interact with page content, including pressing a submit button via
pressButton() method is asynchronous ; it presses the button, submits the form, loads the server response, and executes all browser events until there is no one left. That's why it takes a callback as parameter - and also why the enclosing mocha test must be considered asynchronous as well. Therefore, the mocha
it() test uses a
done callback to be called when the test is finished. Mocha deals with asynchronous functions in
it() just like in
Apparently, this test works fine. However, if any of the assertions ever fails, then the execution flow of the
pressButton() callback stops, and
done() never gets called. So Mocha will report the page as timed out, even though the problem is of another nature.
Zombie supports Promises (powered by q) to overcome this problem. Promises propagate errors implicitly to the last call, and therefore are compatible with tests. Here is how to rewrite the previous test using Promises:
Now if one of the assertion fails and throws an error, the error gets caught in the first then(), then passed passed to the second
then() as argument to the first callback, resulting in
done(error). Mocha will consider the test failed. If no error is thrown, the second callback passed to the last
then() will be called with no argument at all, resulting in
done(). Mocha will consider the test passed.
then(done, done) construction may seem weird at first sight, but it's an efficient way to use the same callback for error and success, and it's the official way to make zombie and mocha work together.
Now that you know how to do asynchronous tests with mocha and zombie.js, it should not be hard to deal with the rest of the tests:
Run the new tests. They should fail. Now you just need to implement enough server-side logic to make the tests pass:
$ mocha contact page ✓ should show a contact form ✓ should refuse empty submissions (207ms) ✓ should refuse partial submissions (138ms) ✓ should keep values on partial submissions (142ms) ✓ should refuse invalid emails (142ms) ✓ should accept complete submissions (143ms) 6 tests complete (1 seconds)
And you're done! Or are you?
You think these tests are correct? Try to change the order in which they are implented by placing the last one first, and run the suite again:
$ mocha contact page ✓ should accept complete submissions (191ms) 1) should show a contact form 2) should refuse empty submissions 3) should refuse partial submissions 4) should keep values on partial submissions 5) should refuse invalid emails ✖ 5 of 6 tests failed: ...
The first test passes, but all the subsequent tests fail. Why? It's simple: the
before() page loads the contact form once and for all. The test "the contact form should accept complete submissions" changes the browser page to the "message sent" page, where the contact form is nowhere to be found. Logically, all the subsequent tests fail because they try to submit a form which doesn't exist.
The solution? Rename the
before() step loading the form to
beforeEach(), so that mocha reloads the contact form before running each
So here is the final contact page functional test:
I'm sure you can write a contact form which satisfies all these tests. But how about testing the fact that the contact form really sends an email to the site owner's address? Well, read again the beginning of this post: the Product Owner never wrote such an acceptance test, so it's another story. Or, it could be a supporting argument for another post labelled "why developers should help product owners write acceptance tests".
Asynchronicity aside, writing functional tests in Node.js is quite simple. Using the assert module, mocha, and zombie.js, you should be able to implement most of the scenarios imagined by the product owner, and never do a manual browser test again.
And despite the relative youth of the Node.js stack, this post shows that Node is already compatible with a test-driven development workflow, and with agile methodologies. So don't wait, jump on the bandwagon!
Published on 15 Jan 2013
with tags agile NodeJS