class FizzBuzz {
generate(number) {
if (number % 3 === 0) {
return 'fizz'
} else if (number % 5 === 0) {
return 'buzz'
}
return number
}
}
describe('FizzBuzz', () => {
let fizzBuzz
beforeEach(() => {
fizzBuzz = new FizzBuzz()
})
it('returns the number for no fizzbuzz', () => {
expect(fizzBuzz.generate(2)).toEqual(2)
})
it('returns "fizz" for 3', () => {
expect(fizzBuzz.generate(3)).toEqual('fizz')
})
it('returns "buzz" for 5', () => {
expect(fizzBuzz.generate(5)).toEqual('buzz')
})
})
Pure functions are easy to test.
And so are classes with few dependencies.
What about this one?
export default class RockPaperScissors {
constructor(ui, humanPlayer, computerPlayer) {
this.ui = ui
this.humanPlayer = humanPlayer
this.computerPlayer = computerPlayer
}
run() {
this.ui.greet()
this.play()
}
// ...
}
But when we look at this...
const consoleInput = require('prompt-sync')()
const consoleOutput = console
const ui = new Ui(consoleInput, consoleOutput)
const game = new RockPaperScissors(ui,
new HumanPlayer(ui),
new ComputerPlayer())
game.run()
Or what about this one?
fetchData() {
const url = `${url}?criteria=${this.filterCriteria()}`
return window.fetch(url,
{credentials: 'same-origin',
cache: 'no-store'}
).then(result => result.json())
}
Let's look at one of our examples again.
const consoleInput = require('prompt-sync')()
const consoleOutput = console
const ui = new Ui(consoleInput, consoleOutput)
const game = new RockPaperScissors(ui,
new HumanPlayer(ui),
new ComputerPlayer())
game.run()
Without test doubles
it('plays game', () => {
const consoleInput = require('prompt-sync')()
const consoleOutput = console
const ui = new Ui(consoleInput, consoleOutput)
const game = new RockPaperScissors(ui,
new HumanPlayer(ui),
new ComputerPlayer())
game.run()
expect('???').toEqual('???')
})
With test doubles
it('plays game', () => {
const uiDummy = new UiDummy()
const fakePlayer = new FakePlayer('paper', ['n'])
const game = new RockPaperScissors(uiDummy,
fakePlayer,
fakePlayer)
game.run()
expect(fakePlayer.makeMoveCallCount()).toEqual(2)
})
class UiDummy {
greet() {}
announceComputerMove(_) {}
announceWinner(_) {}
sayBye() {}
}
export default class HumanPlayer {
constructor(ui) {
this.ui = ui
}
makeMove() {
return this.ui.askForMove()
}
}
export default class Ui {
constructor(input, output) {
this.input = input
this.output = output
}
askForMove() {
this.output.log('Enter your move:\n')
return this.input()
}
}
class UiStub {
askForMove() {
return 'Rock'
}
}
export default class Ui {
constructor(input, output) {
this.input = input
this.output = output
}
greet() {
this.output.log('Welcome')
}
}
it('greets the user', () => {
const outputSpy = new OutputSpy()
const ui = new Ui(inputSpy, outputSpy)
ui.greet()
expect(outputSpy.printedMessage()).
toEqual('Welcome')
})
class OutputSpy {
constructor() {
this.message = ''
}
log(message) {
this.message = message
}
printedMessage() {
return this.message
}
}
class FakePlayer {
constructor(move, replayChoice) {
this.move = move
this.replayChoice = replayChoice
this.count = 0
}
makeMove() {
this.count++
return this.move
}
getReplayChoice() {
return this.replayChoice.shift()
}
makeMoveCallCount() {
return this.count
}
}
Side note: this is not quite so easy with typed languages.
@aebaR