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