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.
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.
const findWordsOfLength = (sentence, count) => {
return sentence.split(' ').filter(word => {
return word.length <= count
})
}
module.exports = findWordsOfLength
const assert = require('assert')
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
This is a test that we expect to fail.
Â
Write a test that proves the bug. And then fix the bug so that the test passes.
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'])
})
npm run our-own-test-framework
// before
assert.deepStrictEqual(foo(), ['The'])
// after
expect(foo()).toEqual(['The'])
npm run using-jest
npx jest --watch or yarn jest --watch
Â
(or, TDD)
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', () => {
})
describe: great for grouping tests and documenting them
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([])
})
})
describe('filtering employees', () => {
it('filters them correctly', () => {
})
})
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', () => {
})
})
expect(personFinder.__internal_thing).toEqual(...)
expect(personFinder.listOfPeople()).toEqual(...)
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
npm run writing-good-tests
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
}
})
}
Refactor the implementation of the employee filter as you see fit, whilst using the tests to ensure you've not broken any behaviour.
npm run refactoring-with-tests -- --watch yarn run refactoring-with-tests --watch
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 },
],
})
}
const findEmployees = filters => {
return fetchEmployees().then(response => {
return response.employees.filter(employee => {
...
})
})
}
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,
},
])
})
})
})
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,
},
])
})
})
})
Â
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,
},
])
})
})
})
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,
},
])
})
})
})
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 => {
we need to swap out fetchEmployees for the purposes of the test
Jest has great support for mocking modules with fake functions for the purposes of tests.
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.
jest.mock('./fetch-employees')
const fetchEmployees = require('./fetch-employees')
fetchEmployees.mockImplementation(() => {
return Promise.resolve({
employees: [
{ name: 'Alice', department: 'engineering', tenure: 1 },
{ name: 'Bob', department: 'HR', tenure: 2 },
],
})
})
Jest will swap the real implementation for the function we give, so we can easily control our employees for this test.
npm run testing-promises
Â
(loading the code in my editor as the slides are too small!)
fetchEmployees.mockImplementationOnce(() => {})
prefer mockImplementationOnce
npm run issues-with-mocks
Don't be afraid to write small helper functions for your tests to help make set-up easier.
npm run tidying-up-tests
Â
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 },
])
})
npm run async-await-promises
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()
}
})
}
}
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()
}
})
}
}
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()
}
})
}
}
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?
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 :)
npm run tests-for-existing-code
fix the test I've left you, and think about any other tests that this module needs.
npm run tests-for-existing-code
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.
npm run using-spies
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
}
}
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')
})
})
beforeEach is new!
npm run testing-the-dom
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++
}
}
jest.useFakeTimers()
jest.runOnlyPendingTimers()
npm run testing-timeouts
there are two main choices in this space
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',
])
})
})
npm run testing-react
npm run employee-finder-react
(this will let you see it in the browser on localhost:1234)
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
npm run test-data-bot