Test-driven development (TDD) is a concept from Extreme Programming and a cornerstone of most agile development methodologies. TDD describes a very short development cycle, which is repeated many times over the development of a module:
- write a failing test case
- make the test case pass
- refactor
- repeat
The goal of TDD is to produce modules that are easy to use and understand and which are fully documented by a suite of unit tests.
When a project has good test coverage, developers can improve the implementation without fear of introducing errors.
Because unit tests don't document all possible interactions between dependent modules, developers also write integration or end-to-end tests, which verify the behaviour of the modules in the whole system.
The three laws of test driven development, by Robert Martin
- You are not allowed to write any production code unless it is to make a failing unit test pass.
- You are not allowed to write any more of a unit test than is sufficient to fail.
- You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
A unit test makes one or more assertions about how the code under test should function. The set of tests that cover a given unit of code should ideally document its entire interface, and its side effects if any. In general we try to avoid testing implementation details such as private functions and service dependencies.
- assertion logical expression to verify ("assert") the module behaves correctly
- interface how an object interacts with the world
- type and number of arguments
- return values
- callbacks
- side effects, such as outputting to a device or service module (db activity, drawing to a screen...)
- implementation non-public parts of a module that could break during refactoring
function truth () {
return true
}
if ('function' !== typeof truth) throw new Error('what')
if (!truth()) throw new Error('nope')
- Mocha
- QUnit interface:
- suite(name) group tests under a label name
- before(fn) call fn before each test
- test(name, fn) call fn; if it throws, it fails
- after(fn) call fn after each test
- assert and timoxley/assert
- Chai
- example module with suite: Point
suite('truth()')
test('returns true all the time', function () {
assert(truth() === true);
)
If you have to test a module with an async interface, Mocha provides a callback function you can invoke to indicate when the test is done:
suite('truth.async()')
test('calls back with true', function (done) {
truth.async(function (t) {
assert(t === true)
done()
})
})
A test double is a pretend or substitute version of an object used for testing.
Types of doubles include:
A mock is an object that contains a set of expectations about how its interface will be used. An example might be a function that can report how many times it has been called, or a function that asserts its arguments will be strings.
Stub objects have minimal implementations that provide canned answers or simple fixture data. An example stubbed object might have a single method that simply returns true
in response to any call.
Objects with working implementations, but are unsuitable for production. In-memory databases, faux services with complex implementations, or real services configured with test credentials are examples.
Mock sparingly; try to design your module's interface so it is loosely coupled to its service dependencies and can be tested in isolation.
-
sinon.js is for easily generating test doubles
-
mockery is for substituting service dependencies with stubbed implementations by augmenting node's
require()
Related to test doubles, fixtures are objects or external data used to verify the function of a module. Examples might be a prepared instance of an object to be tested, a JPEG file used to verify an image resizing function, or a CSV file with fake records to verify a module that writes to a database. As with mocks, use external fixtures sparingly.
- debugging, client and server
- setting breakpoints
- stepping thru
debugger
- visionmedia/debug
- node-inspector
- longjohn