Testing JavaScript

Hello! 👋

The plan

I'll do some talking and demoing on my laptop, and then give you some work to do!

 

Run the exercises on your computer. Feel free to play around with the code, try and break stuff, and explore.

Questions

Please ask lots of questions! This is a day for you all to learn and questions that pop up during the day are often the best bits.

 

We have lots of time for tangents and extra explanations.

Test

Driven

Development

Test

Driven

Development

Use tests in the way that helps you the most.

Let's get started!

GitHub:

node v8+
npm or yarn install

const findWordsOfLength = (sentence, count) => {
  return sentence.split(' ').filter(word => {
    return word.length <= count
  })
}

module.exports = findWordsOfLength

Finding bugs

meet assert

const assert = require('assert')

So you write a test...

npm run intro-hunting-down-bugs
const assert = require('assert')
const findWordsOfLength = require('./index')

const result = findWordsOfLength('The person went walking', 3)
assert.deepStrictEqual(result, ['The'])
const findWordsOfLength = (sentence, count) => {
  return sentence.split(' ').filter(word => {
    return word.length <= count
  })
}

module.exports = findWordsOfLength

Have you found the bug?

Let's write a test for this bug.

This is a test that we expect to fail.

Your turn!

 

Write a test that proves the bug. And then fix the bug so that the test passes.

Testing frameworks

assert is nice

but it would be nice if we could give our tests some structure

const assert = require('assert')
const findWordsOfLength = require('./index')
const { test } = require('./framework')


test('It pulls out the words of the right length', () => {
  const result = findWordsOfLength('The person went walking', 3)
  assert.deepStrictEqual(result, ['The'])
})

test('It finds words only of the given length, not ones that are shorter', () => {
  const result = findWordsOfLength('The person went for a walk', 3)
  assert.deepStrictEqual(result, ['The', 'for'])
})

A nicer structure

implement the test function!

npm run our-own-test-framework

test frameworks are not magic

Some important differences

  • Jest uses the expect() style of assertions
  • It expects files to be named in a certain way: `foo.test.js`
  • You don't import anything - all Jest's functions are globally available.
// before
assert.deepStrictEqual(foo(), ['The'])

// after
expect(foo()).toEqual(['The'])

Expectations

write a test using Jest!

 npm run using-jest

--watch

npx jest --watch or yarn jest --watch

 

Driving an API with tests

(or, TDD)

Filtering employees for your HR software

"as a user I can filter employees by department and/or tenure"

const employees = [
  { name: 'Alice', department: 'Engineering', tenure: 5 },
  { name: 'Bob', department: 'HR', tenure: 2 },
  { name: 'Charlie', department: 'Finance', tenure: 3 },
  { name: 'Danielle', department: 'Engineering', tenure: 1 },
  { name: 'Edward', department: 'Engineering', tenure: 7 },
  { name: 'Grace', department: 'Design', tenure: 4 },
]

const findEmployees = (employees, filters) => {
  // ??
}
const findEmployees = require('./index')

describe('filtering employees', () => {
  
})

Let's explore APIs

describe: great for grouping tests and documenting them

Decide on your API, write a test first, and then implement it.

Bad tests

People who claim that writing tests slows them down are writing incorrect tests.

describe('filtering employees', () => {
  it('is defined', () => {
    expect(findEmployees).not.toBe(undefined)
  })

  it('returns nothing if not given any employees', () => {
    expect(findEmployees([], {})).toEqual([])
  })

  it('returns nothing if given weird data', () => {
    expect(findEmployees(undefined)).toEqual([])
  })
})

Tests with little value


describe('filtering employees', () => {
  it('filters them correctly', () => {
  })
})

Badly written tests

describe('filtering employees', () => {
  it('can filter for people in engineering', () => {
  })
  it('can filter for people in HR', () => {
  })
  it('can filter for people in finance', () => {
  })
  it('can filter for people in design', () => {
  })
})

Too many tests

Don't test internal details, but public APIs

expect(personFinder.__internal_thing).toEqual(...)
expect(personFinder.listOfPeople()).toEqual(...)

Which is best?

OK, so what does a good test look like?

Setup
Invoke
Assert

describe('filtering employees', () => {
  it('can filter for employees based on tenure', () => {
    const allEmployees = [
      { name: 'jack', department: 'engineering', tenure: 2 },
      { name: 'alice', department: 'finance', tenure: 3 },
    ]

    const result = findEmployees(allEmployees, {
      tenure: 2,
    })

    expect(result).toEqual([
      {
        name: 'jack',
        department: 'engineering',
        tenure: 2,
      },
    ])
  })
})
describe('filtering employees', () => {
  it('can filter for employees based on tenure', () => {
    const allEmployees = [
      { name: 'jack', department: 'engineering', tenure: 2 },
      { name: 'alice', department: 'finance', tenure: 3 },
    ]

    const result = findEmployees(allEmployees, {
      tenure: 2,
    })

    expect(result).toEqual([
      {
        name: 'jack',
        department: 'engineering',
        tenure: 2,
      },
    ])
  })
})

SETUP

describe('filtering employees', () => {
  it('can filter for employees based on tenure', () => {
    const allEmployees = [
      { name: 'jack', department: 'engineering', tenure: 2 },
      { name: 'alice', department: 'finance', tenure: 3 },
    ]

    const result = findEmployees(allEmployees, {
      tenure: 2,
    })

    expect(result).toEqual([
      {
        name: 'jack',
        department: 'engineering',
        tenure: 2,
      },
    ])
  })
})

INVOKE

describe('filtering employees', () => {
  it('can filter for employees based on tenure', () => {
    const allEmployees = [
      { name: 'jack', department: 'engineering', tenure: 2 },
      { name: 'alice', department: 'finance', tenure: 3 },
    ]

    const result = findEmployees(allEmployees, {
      tenure: 2,
    })

    expect(result).toEqual([
      {
        name: 'jack',
        department: 'engineering',
        tenure: 2,
      },
    ])
  })
})

ASSERT

write another test and implement filtering by department

 npm run writing-good-tests

Using tests to drive refactoring

const findEmployees = (employees, filters) => {
  return employees.filter(employee => {
    if (filters.department && filters.tenure) {
      return (
        employee.tenure === filters.tenure &&
        employee.department === filters.department
      )
    } else if (filters.department) {
      return employee.department === filters.department
    } else if (filters.tenure) {
      return employee.tenure === filters.tenure
    } else {
      return true
    }
  })
}

This has got a bit messy

But! We have comprehensive tests.

Code editor

Refactor the implementation of the employee filter as you see fit, whilst using the tests to ensure you've not broken any behaviour.

Terminal

npm run refactoring-with-tests -- --watch


yarn run refactoring-with-tests --watch

Is there any tidying we could do to the tests themselves?

Testing promises

const fetchEmployees = () => {
  // imagine that in real life this made a network call to an API
  // which I'm faking here for the purposes of the workshop with Promise.resolve
  return Promise.resolve({
    employees: [
      { name: 'Alice', department: 'Engineering', tenure: 5 },
      { name: 'Bob', department: 'HR', tenure: 2 },
      { name: 'Charlie', department: 'Finance', tenure: 3 },
      { name: 'Danielle', department: 'Engineering', tenure: 1 },
      { name: 'Edward', department: 'Engineering', tenure: 7 },
      { name: 'Grace', department: 'Design', tenure: 4 },
    ],
  })
}

Promises

const findEmployees = filters => {
  return fetchEmployees().then(response => {
    return response.employees.filter(employee => {
      ...
    })
  })
}

findEmployees

describe('filtering employees', () => {
  it('can filter for employees based off tenure', () => {
    return findEmployees({
      tenure: 2,
    }).then(employees => {
      expect(employees).toEqual([
        {
          name: 'Bob',
          department: 'HR',
          tenure: 2,
        },
      ])
    })
  })
})

first pass at a test

describe('filtering employees', () => {
  it('can filter for employees based off tenure', () => {
    return findEmployees({
      tenure: 2,
    }).then(employees => {
      expect(employees).toEqual([
        {
          name: 'Bob',
          department: 'HR',
          tenure: 2,
        },
      ])
    })
  })
})

note how Jest lets you return promises

Jest makes testing promises easy, just return them!

 

We'll circle back and look at more promise based tests in a bit.

describe('filtering employees', () => {
  it('can filter for employees based off tenure', () => {
    return findEmployees({
      tenure: 2,
    }).then(employees => {
      expect(employees).toEqual([
        {
          name: 'Bob',
          department: 'HR',
          tenure: 2,
        },
      ])
    })
  })
})

but there's a bigger problem with this test

describe('filtering employees', () => {
  it('can filter for employees based off tenure', () => {
    return findEmployees({
      tenure: 2,
    }).then(employees => {
      expect(employees).toEqual([
        {
          name: 'Bob',
          department: 'HR',
          tenure: 2,
        },
      ])
    })
  })
})

it uses the real network

what if Bob leaves the company?

this test starts to fail even though

none of our code changed

const findEmployees = filters => {
  return fetchEmployees().then(response => {

it uses the real network

we need to swap out fetchEmployees for the purposes of the test

Mocks

Mocks

Jest has great support for mocking modules with fake functions for the purposes of tests.

Be careful!

Mocking everything can lead to tests that are hard to use or hard to spot issues with, as everything is mocked. Use them with caution.

A mock is a function that tracks how and when it's called, and lets you easily define how it behaves.

Mocking an entire module

jest.mock('./fetch-employees')

const fetchEmployees = require('./fetch-employees')

jest.mock

    fetchEmployees.mockImplementation(() => {
      return Promise.resolve({
        employees: [
          { name: 'Alice', department: 'engineering', tenure: 1 },
          { name: 'Bob', department: 'HR', tenure: 2 },
        ],
      })
    })

mockImplementation

Jest will swap the real implementation for the function we give, so we can easily control our employees for this test.

your turn! mock the fetchEmployees function and write the tests

npm run testing-promises

Gotchas with mocks

can you spot the problem

 

(loading the code in my editor as the slides are too small!)

each individual unit test should be independent

fetchEmployees.mockImplementationOnce(() => {})

prefer mockImplementationOnce

fix the tests by configuring jest

 npm run issues-with-mocks

Writing maintainable tests

Don't be afraid to write small helper functions for your tests to help make set-up easier.

your turn!

 npm run tidying-up-tests

Back to promises

async / await

 

we can use the new async/await syntax in Jest to make testing promises even easier.

  it('can filter for employees based off tenure', async () => {
    mockEmployees([
      { name: 'Bob', department: 'Engineering', tenure: 2 },
      { name: 'Alice', department: 'Design', tenure: 1 },
    ])

    const employees = await findEmployees({ tenure: 2 })
    expect(employees).toEqual([
      { name: 'Bob', department: 'Engineering', tenure: 2 },
    ])
  })

Using async/await

rewrite the tests to use async/await

 npm run async-await-promises

Writing tests for code that already exists.

EventHub

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

You need to refactor this code, but you worry about breaking the functionality.

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

so how do you ensure you cover it with tests?

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

Code coverage

be careful with code coverage!

don't just test stuff for the sake up code coverage

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

3 public functions

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

probably not worth testing this one?

"it has no listeners by default"

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

should test this function!

 

it "lets me add a new event listener"

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

we should test this! This is the main part of this module.

class EventHub {
  constructor() {
    this.listeners = []
  }

  listen(eventName, fn) {
    this.listeners.push({
      eventName,
      fn,
    })
  }

  trigger(eventName) {
    this.listeners.forEach(listener => {
      if (listener.eventName === eventName) {
        listener.fn()
      }
    })
  }
}

but how do we test that a function is called?

First attempt

    let called = false
    const events = new EventHub()

    events.listen('test', () => {
      called = true
    })

    events.trigger('test')

    expect(called).toEqual(true)

PS: there is a better way to do this that you might know about, but we'll cover that next! Don't jump ahead :)

are there any other tests you would write?

 npm run tests-for-existing-code

fix the test I've left you, and think about any other tests that this module needs.

Let's talk through how I'd do it and see how you compare :)

 npm run tests-for-existing-code

Using Jest spies

Testing functions by setting some variable is not very nice.

We can use a Jest spy to do this.

Note: mocks and spies are very similar, and in Jest terms they behave identically.

    const spyListener = jest.fn()
    eventHub.listen('foo', spyListener)

    eventHub.trigger('foo')
    expect(spyListener).toHaveBeenCalledTimes(1)

Spies are functions that know how and when they were called.

rewrite using spies

 npm run using-spies

Testing using the DOM

JSDom

https://github.com/jsdom/jsdom

 

An implementation of the DOM, running in Node.

Supported out of the box in Jest.

class ButtonCounter {
  constructor(rootElement) {
    this.rootElement = rootElement
    this.count = 0
  }

  init() {
    this.button = document.createElement('button')

    this.button.addEventListener('click', () => {
      this.incrementCount()
    })

    this.update()

    this.rootElement.appendChild(this.button)
  }

  incrementCount() {
    this.count++
    this.update()
  }

  update() {
    this.button.innerHTML = this.count
  }
}

A counter

describe('Testing the button counter', () => {
  beforeEach(() => {
    document.body.innerHTML = '<div id="test"></div>'
  })

  it('has an initial count of 0', () => {
    const element = document.getElementById('test')
    const button = new ButtonCounter(element)
    button.init()

    expect(element.querySelector('button').innerHTML).toEqual('0')
  })
})

The first test

beforeEach is new!

beforeEach

Runs automatically before every test in the describe block that it lives in.

beforeEach

Perfect for common test set-up, but be careful not to overuse!

write a test that checks incrementing the number

 npm run testing-the-dom

Testing timeouts

Timeouts can be a pain to test

You should consider if it's worth it: can you just test the function that's called from the timeout? Or do you need to test the actual timeout?

class Incrementor {
  constructor(interval) {
    this.count = 0
    this.incrementIn(interval)
  }

  incrementIn(interval) {
    window.setTimeout(() => {
      this.increment()
      this.incrementIn(interval)
    }, interval)
  }

  currentCount() {
    return this.count
  }

  increment() {
    this.count++
  }
}

The incrementor

jest.useFakeTimers()

jest.runOnlyPendingTimers()

fake out some timers

 npm run testing-timeouts

Testing React

react-testing-library

Enzyme

there are two main choices in this space

I like both libraries :) We use Enzyme at work, but I have used react-testing-library on a personal project.

We'll try react-testing-library today

Testing React is much the same as testing the regular DOM.

We'll render a React component

and assert on the HTML that it renders.

react-testing-library provides some nice helpers for doing this.

describe('EmployeeFinder', () => {
  afterEach(cleanup)

  it('lists all employees by default', () => {
    const employees = [
      { name: 'Jack', department: 'engineering', tenure: 2 },
      { name: 'Alice', department: 'engineering', tenure: 3 },
    ]

    const { container } = render(<EmployeeFinder employees={employees} />)
    const employeeListItems = container.querySelectorAll('li')
    expect(Array.from(employeeListItems).map(item => item.innerHTML)).toEqual([
      'Jack - engineering - 2',
      'Alice - engineering - 3',
    ])
  })
})

can you introduce (and test) a reset button?

 npm run testing-react
 npm run employee-finder-react

(this will let you see it in the browser on localhost:1234)

Generating data for tests

test-data-bot

Your tests should have relevant, accurate test data

But it shouldn't take much effort to come up with that data.

const { build, fake, numberBetween, oneOf } = require('test-data-bot')

const employeeBuilder = build('Employee').fields({
  name: fake(f => f.name.findName()),
  tenure: numberBetween(1, 5),
  department: oneOf('Finance', 'Engineering', 'HR', 'Design'),
})

// random name, tenure and department
const employee = employeeBuilder()

// random tenure, department but name === 'Jack'
const jack = employeeBuilder({ name: 'Jack' })

test-data-bot

last but not least

 npm run test-data-bot

Testing JavaScript

By Jack Franklin

Testing JavaScript

  • 1,087