Chapter 10
Driving with tests
10.1 No tests? Disaster awaits
((function(){var a,b,c=function(a,b){return function(){return a.apply(b,arguments)}};b=require("http"),a=function(){function a(a,b){this.options=a,this.http=b,this.controller=c(this.controller,this),this.pages=[]}return a.prototype.start=function(a){return this.server=this.http.createServer(this.controller),this.server.listen(this.options.port,a)},a.prototype.stop=function(){return this.server.close()},a.prototype.controller=function(a,b){return this.increment(a.url),b.writeHead(200,{"Content-Type":"text/html"}),b.write(""),b.end()},a.prototype.increment=function(a){var b,c;return(c=(b=this.pages)[a])==null&&(b[a]=0),this.pages[a]=this.pages[a]+1},a.prototype.total=function(){var a,b,c,d;c=0,d=this.pages;for(b in d)a=d[b],c+=a;return c},a}(),exports.Tracking=a})).call(this);
10.2 How to write tests
initial = totalVisits
# Somehow visit the homepage
# Assert that the totalVisits has increased by one
assert = require 'assert'
# { [Function: ok] ?
assert.ok 4 is 4
# undefined
assert.ok 4 == 5
# AssertionError: false == true
do assert4Equals4 = ->
assert.ok 4 == 4
do assert4Equals4 = ->
assert.equal 4, 4
<html>
<div class="product special">X12</div>
<html>
<div class="product special popular">X12</div>
assert = require 'assert'
do addWordShouldAddOneWord = ->
input = "product special"
expectedOutput = "product special popular"
actualOutput = addWord input, "popular"
assert.equal expectedOutput, actualOutput
> coffee word_utils.spec.coffee
ReferenceError: addWord is not defined
assert = require 'assert'
{addWord} = require './word_utils'
do addWordShouldAddOneWord = ->
input = "ultra mega"
expectedOutput = "ultra mega ok"
actualOutput = addWord input, "ok"
assert.equal expectedOutput, actualOutput
addWord = (existing, addition) -> # not implemented
exports.addWord = addWord
> coffee word_utils.spec.coffee
AssertionError: "ultra mega ok" == "undefined"
addWord = (text, word) ->
"#{text} #{word}"
> coffee word_utils.spec.coffee
# No output
<html>
<div class="product special popular"></div>
<html>
<div class="product special"></div>
assert = require 'assert'
{addWord, removeWord} = require './word_utils'
do removeWordShouldRemoveOneWord = ->
input = "product special"
expectedOutput = "product"
actualOutput = removeWord input, "special"
assert.equal expectedOutput, actualOutput
removeWord = (text, word) ->
text.replace word, ''
exports.removeWord = removeWord
> coffee word_utils.spec.coffee
# AssertionError: "product" == "product "
removeWord = (text, word) ->
replaced = text.replace word, ''
replaced.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
> coffee word_utils.spec.coffee
# No output
do removeWordShouldRemoveOneWord = ->
tests = [
initial: "product special"
replace: "special"
expected: "product"
,
initial: "product special"
replace: "spec"
expected: "product special"
]
for test in tests
actual = removeWord(test.initial, test.replace)
assert.equal actual, test.expected
> coffee word_utils.spec.coffee
#AssertionError: "product ial" == "product special"
removeWord = (text, toRemove) ->
words = text.split /\s/
(word for word in words when word isnt toRemove).join ' '
> coffee word_utils.spec.coffee
# No output
> coffee word_utils.spec.coffee
>
Listing 10.1 Add and remove word
assert = require 'assert'
{addWord, removeWord} = require './word_utils'
fact = (description, fn) ->
try
fn()
console.log "#{description}: OK"
catch e
console.error "#{description}: \n#{e.stack}"
throw e
fact "addWord adds a word", ->
input = "product special"
expectedOutput = "product special popular"
actualOutput = addWord input, "popular"
assert.equal expectedOutput, actualOutput
fact "removeWord removes a word and surrounding whitespace", ->
tests = [
initial: "product special"
replace: "special"
expected: "product"
,
initial: "product special"
replace: "spec"
expected: "product special"
]
for {initial, replace, expected} in tests
assert.equal removeWord(initial, replace), expected
> coffee word_utils.spec.coffee
addWord adds a word: OK
removeWord removes a word and surrounding whitespace: OK
addClass = (selector, newClass) ->
element = document.querySelector selector
if element.className?
element.className = "#{element.className} #{newClass}"
else
element.className = newClass
fact "addClass adds a class to an element using a selector", ->
addClass '#a .b', 'popular'
actualClass = document.querySelector(selector).className
assert.equals 'product special popular', actualClass
10.3 Dependencies
fact "data is parsed correctly", ->
extractData 'support', (res) ->
assert.equals res.status, 'online'
{ status: 'online' }
http = require 'http'
extractData = (topic, callback) ->
options =
host: 'www.agtronsemporium.com'
port: 80
path: "/service/#{topic}"
http.get(options, (res) ->
callback res.something
).on 'error', (e) ->
console.log e
> AssertionError: "undefined" == "online"
class Actor
soliloquy: ->
'To be or not to be...'
stunt: ->
'My arm! Call an ambulance.'
scene = ->
imaStarr = new Actor
imaStarr.soliloquy()
imaStarr.stunt()
'Scene completed'
fact 'The scene is completed', ->
assert.equals scene(), 'Scene completed'
scene = (actor) ->
actor.soliloquy()
actor.stunt()
'Scene completed'
actorDouble =
soliloquy: ->
stunt: ->
scene = (actor) ->
actor.soliloquy()
actor.stunt()
'Scene completed'
fact 'The scene is completed', ->
actorDouble =
soliloquy: ->
stunt: ->
assert.equal scene(actorDouble), 'Scene completed'
# The scene is completed: OK
http = require 'http'
extractData = (topic, callback) ->
options =
host: 'www.agtronsemporium.com'
port: 80
path: "/data/#{topic}"
http.get(options, (res) ->
callback res
).on 'error', (e) ->
console.log e
http =
get: (options, callback) ->
callback 'canned response'
@
on: (event, callback) ->
# do nothing
http = require 'http'
extractData = (topic, http, callback) ->
options =
host: 'www.agtronsemporium.com'
port: 80
path: "/data/#{topic}"
http.get(options, (res) ->
callback res.something
).on 'error', (e) ->
console.log e
class Form
reloader = ->
window.location.reload()
window =
location:
href: ''
reload: ->
closed: false
screen:
top: 0
left: 0
# hundreds of other properties not shown here
windowDouble = Object.create window
windowDouble.location.reload = ->
class Form
constructor: (@window) ->
reloader: ->
@window.location.reload()
fetchData (http, callback) ->
options =
host: 'www.agtronscameras.com'
port: 80
path: "/data/#{topic}"
http.get(options, (res) ->
callback res
).on 'error', (e) ->
console.log e
extractData = (data) ->
console.log data
fetchAndExtractData = ->
fetchData http, extractData
Listing 10.2 Testing with a double
assert = require 'assert'
fact = (description, fn) ->
try
fn()
console.log "#{description}: OK"
catch e
console.log "#{description}: \n#{e.stack}"
http =
get: (options, callback) ->
callback "canned response"
@
on: (event, callback) ->
fetch = (topic, http, callback) ->
options =
host: 'www.agtronscameras.com'
port: 80
path: "/data/#{topic}"
http.get(options, (result) ->
callback result
).on 'error', (e) ->
console.log e
parse = (data) ->
"parsed canned response"
fact "data is parsed correctly", ->
parsed = parse 'canned response'
assert.equal parsed, "parsed canned response"
fact "data is fetched correctly", ->
fetch "a-topic", http, (result) ->
assert.equal result, "canned response"
visits = (database, http, user) ->
http.hitsFor database.userIdFor(user.name)
visits = (database, http, user, permissions) ->
if permissions.allowDataFor user
http.hitsFor database.userIdFor(user.name)
else
'private'
visits = (dependencies) ->
if dependencies.permissions.allowDataFor user
dependencies.http.hitsFor dependencies.database.userIdFor(user.name)
else
'private'
visits = (database, http, user, permissions) ->
if permissions.allowDataFor user
http.hitsFor database.userIdFor(user.name)
else
'private
makeVisitsForUser = (database, http) ->
(user, permissions) ->
visits database, http, user, permissions
database = new Database
http = new Http
visitsForUser = makeVisitsForUser bob, permissions
bob = new User
permissionsForBob = new Permissions
visitsForUser bob, permissionsForBob
fact 'Visits not shown when permissions are private', ->
database = databaseDouble
http = httpDouble
user = userDouble
permissions = new Permissions
assert.equal visits(database, http, user, permissions), 'private'
fact 'Returns visits for user', ->
database = databaseDouble
http = httpDouble
user = userDouble
permissions = new Permissions
assert.equal visits(database, http, user, permissions), 'private'
database = databaseDouble
http = httpDouble
visitsForUser = makeVisitsForUser database, http
fact 'Visits not returned when permissions are private', ->
user = new User
privatePermissions = new Permissions private: true
assert.equal visitsForUser(user, privatePermissions), 'private'
fact 'Visits returned for user', ->
user = new User
privatePermissions = new Permissions private: true
assert.equal visitsForBob(database, http), 'private'
10.4 Testing the asynchronous
fact 'the tracking application tracks a user mouse click', ->
options = {}
tracking = new Tracking options, http
tracking.start ->
fred.visit '/page/1'
assert.equal tracking.total(), 1
fact 'the tracking application tracks a user mouse click', ->
options = {}
tracking = new Tracking options, http
tracking.start ->
assert.equal tracking.total(), 0
fred = new User serverOptions, http
fred.visit '/page/1', ->
fred.clickMouse ->
assert.equal tracking.total(), 1
tracking.stop()
http =
get: (options, callback) ->
callback()
@
on: (event, callback) ->
# do nothing
fact 'the tracking application tracks a user mouse click', ->
options = {}
http =
get: (options, callback) ->
callback()
@
on: (event, callback) ->
# do nothing
tracking = new Tracking options, http
tracking.start ->
fred.visit '/page/1'
assert.equal tracking.total(), 1
fact 'the tracking application tracks a user mouse click', ->
tracking = new Tracking options, http
tracking.start = (callback) ->
callback()
fred.visit '/page/1'
assert.equal tracking.total(), 1
fact 'http service should be accessed', ->
getWasCalled = false
http =
get: ->
getWasCalled = true
invokeTheThingThatGets()
assert.ok getWasCalled
exports.fact = (description, fn) ->
try
fn()
console.log "#{description}: OK"
catch e
console.error "#{description}: "
throw e
Listing 10.3 Test for the Tracking
class
assert = require 'assert'
{fact} = require './fact'
{Tracking} = require './10.4'
fact 'controller responds with 200 header and empty body', ->
request = url: '/some/url'
response =
write: (body) ->
@body = body
writeHead: (status) ->
@status = status
end: ->
@ended = true
tracking = new Tracking
for view in [1..10]
tracking.controller request, response
assert.equal response.status, 200
assert.equal response.body, ''
assert.ok response.ended
assert.equal tracking.pages['/some/url'], 10
fact 'increments once for each key', ->
tracking = new Tracking
tracking.increment 'a/page' for i in [1..100]
tracking.increment 'another/page'
assert.equal tracking.pages['a/page'], 100
assert.equal tracking.total(), 101
fact 'starts and stops server', ->
http =
createServer: ->
@created = true
listen: =>
@listening = true
close: =>
@listening = false
tracking = new Tracking {}, http
tracking.start()
assert.ok http.listening
tracking.stop()
assert.ok not http.listening
Listing 10.4 The Tracking
class
http = require 'http'
class Tracking
constructor: (@options, @http) ->
@pages = []
start: (callback) ->
@server = @http.createServer @controller
@server.listen @options.port, callback
stop: ->
@server.close()
controller: (request, response) =>
@increment request.url
response.writeHead 200, 'Content-Type': 'text/html'
response.write ''
response.end()
increment: (key) ->
@pages[key] ?= 0
@pages[key] = @pages[key] + 1
total: ->
sum = 0
for page, count of @pages
sum = sum + count
sum
exports.Tracking = Tracking
fact 'http service should be accessed', ->
httpDouble = double http
tracking = new Tracking {}, httpDouble
assert.ok httpDouble.listen.called == true
10.5 System tests
fact 'the tracking application tracks a single mouse click', ->
tracking.start ->
assert.equals tracking.total, 0
userVisitsPageAndClicks()
assert.equals tracking.total, 1
Listing 10.5 System test for tracking application
assert = require 'assert'
{fact} = require './fact'
http = require 'http'
{Tracking} = require './10.4'
{User} = require './10.6'
SERVER_OPTIONS =
host: 'localhost'
port: 8080
fact 'the tracking application tracks a user mouse click', ->
tracking = new Tracking SERVER_OPTIONS, http
tracking.start ->
assert.equal tracking.total(), 0
fred = new User SERVER_OPTIONS, http
fred.visitPage '/some/url', ->
fred.clickMouse ->
assert.equal tracking.total(), 1
tracking.stop()
Listing 10.6 The User
class
class User
constructor: (@options, @http) ->
visitPage: (url, callback) ->
@options.path = url
@options.method = 'GET'
callback()
clickMouse: (callback) ->
request = @http.request @options, (request, response) ->
callback()
request.end()
exports.User = User
10.6 Test suites
> coffee test1.coffee
# OK
> coffee test2.coffee
# OK
> coffee test3.coffee
# Fail
fact 'this test uses a http double', ->
http =
get: (options, callback) ->
callback()
@
on: (event, callback) ->
# do nothing
httpPrototype =
get: (options, callback) ->
callback()
@
on: (event, callback) ->
# do nothing
fact 'this test uses a http double', ->
http = Object.create httpPrototype
fact 'this test also uses a http double', ->
http = Object.create httpPrototype
createHttp = ->
http =
get: (options, callback) ->
callback()
@
on: (event, callback) ->
# do nothing
http
fact 'this test uses a http double' ->
http = createHttp()
# the rest of the test goes here
do ->
http = {}
setup = ->
http =
get: (options, callback) ->
callback()
@
on: (event, callback) ->
# do nothing
fact 'this test uses a http double' ->
setup()
# the rest of the test goes here
fact 'this test also uses a http double' ->
setup()
# the rest of the test goes here
> coffee word_utils.spec.coffee
# OK
> coffee tracking.spec.coffee
# OK
> coffee another.spec.coffee
# OK
Listing 10.7 The test helper
dependencies =
'Tracking': '../tracking'
'User': '../user'
'fact': '../fact'
for dependency, path of dependencies
exports[dependency] = require(path)[dependency]
Listing 10.8 The test runner
fs = require 'fs'
coffee = require 'coffee-script'
test = (file) ->
fs.readFile file, 'utf-8', (err, data) ->
it = """
{fact} = require './fact'
assert = require 'assert'
#{data}
"""
coffee.run it, filename: file
spec = (file) ->
/[^.]*\.spec\.coffee$/.test file
fs.readdir '.', (err, files) ->
for file in files
test file if spec file
|——tracking/
| |—— test.coffee
| |—— spec/
| | |—— tracking.spec.coffee
| | |—— 2.spec.coffee
| | |—— 3.spec.coffee
| |—— src/
| | |—— tracking.coffee
| | |—— 2.coffee
| | |—— 3.coffee
> coffee test.coffee
> ./test
#!/bin/bash
set -eu
TEST_DIR=$(dirname "$0")
main () {
coffee test.coffee
}
main "$@"
> ./autotest
fact 'True should equal false', ->
assert.equal true, false
> True should equal false
> Assertion Error: true != false
> at spec/1.spec.coffee
Listing 10.9 The watcher
#!/usr/bin/env coffee
coffee = require 'coffee-script'
fs = require 'fs'
SPEC_PATH = './spec/'
SRC_PATH = './src/'
test = (file) ->
fs.readFile SPEC_PATH + file, 'utf-8', (err, data) ->
it = """
{fact} = require '../fact'
assert = require 'assert'
#{data}
"""
coffee.run it, filename: SPEC_PATH + file
spec = (file) ->
if /#/.test file then false
else /\.spec\.coffee$/.test file
tests = ->
fs.readdir SPEC_PATH, (err, files) ->
for file in files
test file if spec file
fs.watch SPEC_PATH, (event, filename) ->
tests()
fs.watch SRC_PATH, (event, filename) ->
tests()
10.7 Summary