Of spies, fakes and friends

Help your code lead a double life

Rabea Gleissner

Software developer

8l-logo

@aebaR on Twitter

TDD is not always easy

Here's one that is quite straight foward...


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

And that is why we use...

🎉 Test doubles 🎉

What is a test double?

  • An object that stands in for your production code objects in tests
  • Needs to conform to the interface of the required collaborator

Why do we use them?

  • To reduce the amount of dependencies
  • To make impure functions pure
  • To make tests less fragile
  • To force our code to execute certain branches

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

Types of test doubles

  • Dummy
  • Stub
  • Spy
  • Fake

Why does this differentiation matter?

Rock, Paper, Scissors

Dummy

  • Implements any functions that are called
  • Usually empty function body
  • Unless it has to return something

Dummy example


          class UiDummy {
            greet() {}
            announceComputerMove(_) {}
            announceWinner(_) {}
            sayBye() {}
          }
          

Stub

  • Simplified implementation of a method
  • Returns a value but without business logic
  • Can force code to run along specific paths

Stub example


          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'
            }
          }
          

Spy

  • Records some information
  • To check if a method was called
  • How many times a method was called
  • Number of method arguments
  • Check that the arguments are correct

Spy example


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

Fake

  • Has a working implementation
  • But is a shortcut
  • Not suitable for production code

Fake example


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

Double

  • All of the above

Side note: this is not quite so easy with typed languages.

Test frameworks have all this built in

Thank you!

@aebaR