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!
jackfranklin/
testing-javascript-workshop
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