Make testing suck less

Write your tests upfront

Make your tests fast

  • Isolate your tests from IO as much as possible.
  • Only run the tests your are working on.

Stop testing internals

Make your tests communicate your intention

describe('resize', () => {
  describe('when only given a width', () => {
    it ('resizes images proportionally')
      return expect(
        './2048x1324.jpg',
        'when scaled to', { width: 1024 },
        'to have metadata satisfying', {
          format: 'JPEG',
          size: { width: 1024, height: 662 }
        }
      })
    })
  })
})

Choose a tool that gives you great feedback

expect([0, 1, 2, 4, 5], 'to equal', [1, 2, 3, 4])
expected [ 01245 ] to equal [ 1234 ]
 
[
  
0// should be removed
1,
2,
//
 
missing 
3
4,
5 // should be removed
]

Stop testing at the wrong level of abstraction

const atf = 'AT THE FRONTEND'
expect(atf.indexOf('FRONTEND') !== -1, 'to be true')

instead of

expect(atf, 'to contain', 'FRONTEND')

Take it to the next level

Tic Tac Toe

import Game from 'tic-tac-toe-js/lib/game'
 
const game = new Game()
 
game.move('O', 9)                                           // X | O |
game.move('X', 6)                                           // --+---+--
game.move('O', 8)                                           //   | O | X
game.move('X', 7)                                           // --+---+--
game.move('O', 5)                                           // X | O | O
game.move('X', 1)
game.move('O', 2)

expect(
  game.at(0) === 'X' &&                                     // X | O |
  game.at(1) === 'O' &&                                     // --+---+--
  game.at(2) === null &&                                    //   | O | X
  game.at(3) === null &&                                    // --+---+--
  game.at(4) === 'O' &&                                     // X | O | O
  game.at(5) === 'X' &&
  game.at(6) === 'X' &&
  game.at(7) === 'O' &&
  game.at(8) === 'O',
  'to be true'
)

expected false to be true

expect(game.at(0), 'to be', 'X')                            // X | O |
expect(game.at(1), 'to be', 'O')                            // --+---+--
expect(game.at(2), 'to be null')                            //   | O | X
expect(game.at(3), 'to be null')                            // --+---+--
expect(game.at(4), 'to be', 'O')                            // X | O | O
expect(game.at(5), 'to be', 'X')
expect(game.at(6), 'to be', 'X')
expect(game.at(7), 'to be', 'O')
expect(game.at(8), 'to be', 'O')

expected null to be 'X'

expect([                                                    // X | O |
  game.at(0), game.at(1), game.at(2),                       // --+---+--
  game.at(3), game.at(4), game.at(5),                       //   | O | X
  game.at(6), game.at(7), game.at(8)                        // --+---+--
], 'to equal', [                                            // X | O | O
  'X',  'O', null,
  null, 'O', 'X',
  'X',  'O', 'O'
])

expected [ null'X''O'nullnull'O''X''X''O' ]
to equal [ 'X''O'nullnull'O''X''X''O''O' ]
 
[
┌───▷
│ ┌─▷
│ │
└─│──
  └──
 
 
 
null,
 
'X'// should be moved
 
'O'// should be moved
 
null,
 
null// should be removed
 
'O',
 
'X',
 
'X',
 
'O'
 
//
 
missing 
'O'
]

expect(                                                     // X | O |
  game.toString(),                                          // --+---+--
  'to equal', [                                             //   | O | X
    'X | O |  ',                                            // --+---+--
    '--+---+--',                                            // X | O | O
    '  | O | X',
    '--+---+--',
    'X | O | O'
  ].join('\n')
)

Let's make it fail

expect(                                                     // X | O |
  game.toString(),                                          // --+---+--
  'to equal', [                                             //   | O | X
    'O | X |  ',                                            // --+---+--
    '--+---+--',                                            // X | O | O
    '  | O | X',
    '--+---+--',
    'X | O | O'
  ].join('\n')
)

expected 'X | O |  \n--+---+--\n  | O | X\n--+---+--\nX | O | O'
to equal 'O | X |  \n--+---+--\n  | O | X\n--+---+--\nX | O | O'
 
X | O |  
O | X |  
--+---+--
  | O | X
--+---+--
X | O | O

expect.addType({
  name: 'game',
  base: 'object',
  identify: (value) => value instanceof Game,
  inspect: (game, depth, output) => output.text(game.toString())
})

expect.addAssertion('<game> to satisfy <string>',
                    (expect, game, expectation) => {
  expect.argsOutput[0] = output => output.text(expectation)
  expect(game.toString(), 'to equal', expectation)
})

expect(                                                     // X | O |
  game,                                                     // --+---+--
  'to satisfy', [                                           //   | O | X
    'O | X |  ',                                            // --+---+--
    '--+---+--',                                            // X | O | O
    '  | O | X',
    '--+---+--',
    'X | O | O'
  ].join('\n')
)

expected
X | O |  
--+---+--
  | O | X
--+---+--
X | O | O
to satisfy
O | X |  
--+---+--
  | O | X
--+---+--
X | O | O
 
X | O |  
O | X |  
--+---+--
  | O | X
--+---+--
X | O | O

Pimp it a bit

Pimp it

expect.addAssertion('<game> to satisfy <string>',
                    (expect, game, expectation) => {
  expect.errorMode = 'bubble'
  const actual = boardToJS(game.toString())
  const expected = boardToJS(expectation)
  if (!expect.equal(actual, expected)) {
    expect.fail(output => {
      output.boardDiff(expected, actual)
    })
  }
})

expect(
  game,
  'to satisfy',
  `O | X |  
   --+---+--
     | O | X
   --+---+--
   X | O | O`
)

Expected
O | X |  
--+---+--
  | O | X
--+---+--
X | O | O
   
Actual
X | O |  
--+---+--
  | O | X
--+---+--
X | O | O

expect(
  `O | X | O
   --+---+--
   X |   |  
   --+---+--
   O | X | O`,
  'when placing X at position', 5,
  'to satisfy',
  `O | X | O
   --+---+--
   X |   | X
   --+---+--
   O | X | O`
)

When placing X at position 5 on board:
O | X | O
--+---+--
X | X |  
--+---+--
O | X | O
 
Expected
O | X | O
--+---+--
X |   | X
--+---+--
O | X | O
   
Actual
O | X | O
--+---+--
X | X |  
--+---+--
O | X | O

expect(
  `  |   | 
   --+---+--
   X | O | X
   --+---+--
   O | X | O`,
  'when placing O at position', 3,
  'to have O as the winner'
)
When placing O at position 3 on board:
  |   | O
--+---+--
X | O | X
--+---+--
O | X | O
 
expected
  |   | O
--+---+--
X | O | X
--+---+--
O | X | O
to have O as the winner
  
the game is not finished yet

Testing React components

with unexpected-react


Dave Brotherstone - @bruderstein

import React, { Component } from 'react'
 
class Counter extends Component {
  constructor (props) { super(props); this.state = { value: 0 } } 
  increment = () => this.setState({ value: this.state.value + 1 })
  decrement = () => this.setState({ value: this.state.value - 1 })
 
  render = () => (
    <div>
      <button onClick={this.increment}>+</button>
      <div className='value'>{`${this.state.value}`}</div>
      <button onClick={this.decrement}>-</button>
    </div>
  )
}

import TestUtils from 'react-addons-test-utils'
 
const rendered = TestUtils.renderIntoDocument(
  <Counter/>
)
 
expect(
  rendered,
  'to have rendered',
  <div>
    <button>+</button>
    <div className='value'>0</div>
    <button>-</button>
  </div>
)

Only mention your needs

expect(
  rendered,
  'to have rendered',
  <div><div className='value'>0</div></div>
)
expect(
  rendered,
  'queried for', 
  <div className='value'/>,
  'to have rendered',
  <div className='shiny'/>
)
expected
<Counter>
  
<div>
  
<button onClick={function () { /* ... */ }}>
+
</button>
<div className="value">
0
</div>
<button onClick={function () { /* ... */ }}>
-
</button>
</div>
</Counter>
queried for <div className="value" /> to have rendered <div className="shiny" />
 
<div className="value" 
//
 
missing class 'shiny'
>
  
0
</div>
expect(
  rendered,
  'queried for', 
  <div className='value'/>,
  'to have rendered',
  <div>{ expect.it('to match', /\d+/) }</div>
)
expect(
  rendered,
  'with event click', 'on', <button>+</button>,
  'with event click', 'on', <button>-</button>,
  'with event click', 'on', <button>+</button>,
  'with event click', 'on', <button>+</button>,
  'to contain', <div className='value'>2</div>
)

expect(
  rendered,
  'with event click', 'on', <button>-</button>,
  'to contain', <div className='value'>3</div>
)

expected
<Counter>
  
<div>
  
<button onClick={function () { /* ... */ }}>
+
</button>
<div className="value">
1
</div>
<button onClick={function () { /* ... */ }}>
-
</button>
</div>
</Counter>
with event 'click' on <button>
-
</button> to contain <div className="value">
3
</div>
 
the best match was
<div className="value">
  
1
 
//
//
 
1
3
</div>

Testable documentation

MDN: Object.keys

vs

expect(Object.keys(['a', 'b', 'c']), 'to equal', ['0', '1', '2'])

unexpected-markdown

  • Examples are executed in the content of the examples before it
  • Support for async examples by returning a promise
  • You can skip examples
  • You can assert that an example throw a given error
  • Source maps

Example

http://unexpected.js.org

The end


http://unexpected.js.org

@sunesimonsen / @unexpectedjs