Skip to content

Instantly share code, notes, and snippets.

@lvivier
Last active August 29, 2015 13:56
Show Gist options
  • Save lvivier/8941507 to your computer and use it in GitHub Desktop.
Save lvivier/8941507 to your computer and use it in GitHub Desktop.
Talking about TDD with the team

Test-driven development

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:

  1. write a failing test case
  2. make the test case pass
  3. refactor
  4. 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

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.

Unit tests

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

naive unit test

function truth () {
  return true
}

if ('function' !== typeof truth) throw new Error('what')
if (!truth()) throw new Error('nope')

libs

  • 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

example

suite('truth()')
test('returns true all the time', function () {
  assert(truth() === true);
)

async tests

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()
  })
})

Test doubles

A test double is a pretend or substitute version of an object used for testing.

Types of doubles include:

mocks

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.

stubs

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.

fakes

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.

Using test doubles

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()

Fixtures

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.


Bonus round: debugging

  • debugging, client and server
    • setting breakpoints
    • stepping thru
    • debugger
    • visionmedia/debug
    • node-inspector
    • longjohn
module.exports = Point
/**
* Point
*/
function Point (lat, lng) {
// constructor guard
if (!(this instanceof Point)) return new Point(lat, lng)
// construct from an array
if (Array.isArray(lat)) lng = lat[1], lat = lat[0]
this.lat = this[0] = range(lat, 90, 'latitude')
this.lng = this[1] = range(lng, 180, 'longitude')
}
/**
* Serializes as an array
* @return Array
*/
Point.prototype.toJSON = function () {
return [this.lat, this.lng]
}
/**
* String representation
* @return String
*/
Point.prototype.toString = function () {
return this.toJSON().join(', ')
}
/**
* Range setter
* @api private
*/
function range (val, max, name) {
val = parseFloat(val, 10)
if (isNaN(val)) throw new TypeError('Expected Number')
if (Math.abs(val) > max) throw new RangeError('Invalid '+name)
return val
}
--reporter spec
--ui qunit
/**
* Dependencies
*/
var assert = require('assert')
var Point = require('.')
/**
* Fixture
*/
var lat = 32.715
var lng = -117.1625
var p = new Point(lat, lng)
suite('Point')
test('is a constructor', function () {
assert(p instanceof Point)
assert(p.lat = lat)
assert(p.lng = lng)
})
test('accepts an array of arguments', function () {
var q = new Point([lat, lng])
assert(q instanceof Point)
assert(q.lat = lat)
assert(q.lng = lng)
})
test('constructs if called without new', function () {
var p = Point(lat, lng)
assert(p instanceof Point)
})
test('latitude must be a number between -90 and 90', function () {
assert.throws(function(){ new Point('foo', 0) }, TypeError)
assert.throws(function(){ new Point(-91, 0) }, RangeError)
assert.throws(function(){ new Point(91, 0) }, RangeError)
})
test('longitude must be a number between -180 and 180', function () {
assert.throws(function(){ new Point(0, 'bar') }, TypeError)
assert.throws(function(){ new Point(0, -181) }, RangeError)
assert.throws(function(){ new Point(0, 181) }, RangeError)
})
test('has lat property', function () {
assert(p.lat)
})
test('has lng property', function () {
assert(p.lng)
})
test('has indexes', function () {
assert(p[0] && p[1])
})
test('coerces to String', function () {
var label = 'point: ' + p
assert.equal('point: 32.715, -117.1625', label)
})
test('JSON encodes to Array', function () {
var json = JSON.stringify(p)
assert.equal(json, '[32.715,-117.1625]')
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment