Slide 1

Slide 1 text

Tip top JavaScript Testing @Jack_Franklin

Slide 2

Slide 2 text

Why test? How to test? Mistakes in tests Testing React

Slide 3

Slide 3 text

Why test?

Slide 4

Slide 4 text

TDD

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

! Please write a function to talk to our API for finding items within a certain price range

Slide 7

Slide 7 text

import itemFinder from './item-finder' describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) expect(result).toEqual(...) }) })

Slide 8

Slide 8 text

import itemFinder from './item-finder' describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) expect(result).toEqual(...) }) })

Slide 9

Slide 9 text

const result = itemFinder(dummyItems).min(1000).max(5000) const result = itemFinder(dummyItems, { min: 1000, max: 5000 }) const result = itemFinder(dummyItems).moreThan(1000).lessThan(5000) const result = itemFinder(dummyItems).filter({ lessThan: 5000, moreThan: 1000 }) const result = itemFinder(dummyItems).filter({ gt: 1000, lt: 5000 })

Slide 10

Slide 10 text

TDD lets you test the API before you use it.

Slide 11

Slide 11 text

But beware!

Slide 12

Slide 12 text

Sometimes you might not know what you actually need ! Please write a function to talk to our API that gets items for the browse page

Slide 13

Slide 13 text

it('does something but we do not know what yet', () => { })

Slide 14

Slide 14 text

Here you'll end up writing tests that guess

Slide 15

Slide 15 text

So you're better off actually writing it

Slide 16

Slide 16 text

class BrowsePage extends Component { componentDidMount() { itemsFinder({ total: 20, minPrice: 5000, ... }) } }

Slide 17

Slide 17 text

class BrowsePage extends Component { componentDidMount() { itemsFinder({ total: 20, minPrice: 5000, ... }) } }

Slide 18

Slide 18 text

! it('lets me pass in the total items to return', () => { }) it('takes a minPrice option to filter by price', () => { })

Slide 19

Slide 19 text

Fixing bugs for good There's a bug where the price filtering max price limit is not used

Slide 20

Slide 20 text

1. Prove it in a failing test it('filters by max price correctly', () => { const items = [{ name: 'shirt', price: 3000 }] expect(itemFinder({ maxPrice: 2000})).toEqual([]) })

Slide 21

Slide 21 text

‼ TEST FAILURE: Expected [], got [ { name: 'shirt', price: 3000 }]

Slide 22

Slide 22 text

This is good! We've recreated and isolated the bug, and we can debug without having to manually click around a browser.

Slide 23

Slide 23 text

2. Fix the bug without changing the test

Slide 24

Slide 24 text

3. Rerun the test ✅ TEST PASSED Expected [], got []

Slide 25

Slide 25 text

Confident refactoring

Slide 26

Slide 26 text

Red Green Refactor

Slide 27

Slide 27 text

1: Write the test and see it fail. 2: Write whatever code it takes to make it pass. 3: Rewrite the code until you're happy, using the tests to guide you.

Slide 28

Slide 28 text

1: Write the test and see it fail. 2: Write whatever code it takes to make it pass. 3: Rewrite the code until you're happy, using the tests to guide you.

Slide 29

Slide 29 text

1: Write the test and see it fail. 2: Write whatever code it takes to make it pass. 3: Rewrite the code until you're happy, using the tests to guide you.

Slide 30

Slide 30 text

Tests let you rewrite, tweak and refactor with a quick feedback loop that everything is working as expected.

Slide 31

Slide 31 text

Why test? How to test? Mistakes in tests Testing React

Slide 32

Slide 32 text

How to test

Slide 33

Slide 33 text

It's important your application code is well written and maintainable. But you can put up with rough edges because it's well tested.

Slide 34

Slide 34 text

You don't write tests for your tests So your test code should be !

Slide 35

Slide 35 text

Unless you write tests for your tests

Slide 36

Slide 36 text

But of course then you need tests for your tests for your tests

Slide 37

Slide 37 text

What makes a great test?

Slide 38

Slide 38 text

it('clearly says what is being tested', () => { // 1. Setup // 2. Invoke the code under test // 3. Assert on the results of step 2. })

Slide 39

Slide 39 text

describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) expect(result).toEqual(...) }) })

Slide 40

Slide 40 text

describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) expect(result).toEqual(...) }) })

Slide 41

Slide 41 text

describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) expect(result).toEqual(...) }) })

Slide 42

Slide 42 text

describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) expect(result).toEqual(...) }) })

Slide 43

Slide 43 text

describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) expect(result).toEqual(...) }) })

Slide 44

Slide 44 text

You should be able to look at a single it test and know everything

Slide 45

Slide 45 text

A less good test from the Thread codebase. it('finds the value for the given cookie name', () => { const result = getCookie('foo'); expect(result).toEqual('bar'); });

Slide 46

Slide 46 text

Where is foo=bar coming from? describe('getCookie', () => { document.cookie = 'foo=bar'; it('finds the value for the given cookie name', () => { const result = getCookie('foo'); expect(result).toEqual('bar'); }); });

Slide 47

Slide 47 text

it('finds the value for the given cookie name', () => { document.cookie = 'foo=bar'; const result = getCookie('foo'); expect(result).toEqual('bar'); });

Slide 48

Slide 48 text

Tests should have no external dependencies

Slide 49

Slide 49 text

Network requests

Slide 50

Slide 50 text

it('fetches the 5 items from our API', async () => { const items = await fetchItems(); expect(items.length).toEqual(5) })

Slide 51

Slide 51 text

And then one day: commit 81bc0dbedab785ac86be7f8c23e1416db0b99a4e (tag: v19213) Author: Jack Date: Wed Aug 1 17:10:21 2018 +0100 Make items API return first 10, not 5 items

Slide 52

Slide 52 text

If your tests can fail without any of your code changing, that is bad.

Slide 53

Slide 53 text

You can use a library like fetch-mock for this https://github.com/wheresrhys/fetch-mock it('fetches the 5 items from our API', async () => { fetchMock.get('/items', { status: 200, body: [{ ... }, { ... }, ...] }) const items = await fetchItems(); expect(items.length).toEqual(5) })

Slide 54

Slide 54 text

But there's an often cleaner alternative...

Slide 55

Slide 55 text

Spies and mocks

Slide 56

Slide 56 text

Let's say we're testing a component that fetches items, and then sorts them somehow: const itemSorter = () => { // itemFetcher makes a request to the API return itemFetcher().then(items => { return items.sort(..) }) }

Slide 57

Slide 57 text

it('sorts the items in price ascending order', async () => { // we need to fake the network request! // as this will call `itemFetcher()` const sortedItems = await itemSorter() })

Slide 58

Slide 58 text

Mocking Fake a function's implementation for the purpose of a test.

Slide 59

Slide 59 text

In ours, we're testing the itemSorter, so we can mock out the itemFetcher 1 import itemFetcher from './item-fetcher' jest.mock('./item-fetcher') it('sorts the items in price ascending order', async () => { itemFetcher.mockResolvedValue( [ {name: 'shirt', ...} ] ) const sortedItems = await itemSorter() expect(sortedItems).toEqual(...) }) 1 This code is Jest specific, but all frameworks work similarly here :)

Slide 60

Slide 60 text

import itemFetcher from './item-fetcher' jest.mock('./item-fetcher') it('sorts the items in price ascending order', async () => { itemFetcher.mockResolvedValue( [ {name: 'shirt', ...} ] ) const sortedItems = await itemSorter() expect(sortedItems).toEqual(...) })

Slide 61

Slide 61 text

Mocks give you a lot of power That you should wield carefully.

Slide 62

Slide 62 text

Why test? How to test? Mistakes in tests Testing React

Slide 63

Slide 63 text

Mistakes in tests

Slide 64

Slide 64 text

beforeEach

Slide 65

Slide 65 text

beforeEach is a great way to run code before each test But it can make a test hard to work with or debug.

Slide 66

Slide 66 text

it('filters the items to only shirts', () => { const result = filterItems(items, 'shirts') expect(result).toEqual(...) })

Slide 67

Slide 67 text

Where is items coming from?

Slide 68

Slide 68 text

let items beforeEach(() => { items = [{ name: 'shirt', ... }, ... ] })

Slide 69

Slide 69 text

Fixing this it('filters the items to only shirts', () => { const items = [{ name: 'shirt', ... }, ... ] const result = filterItems(items, 'shirts') expect(result).toEqual(...) })

Slide 70

Slide 70 text

Keep test set-up close to the test.

Slide 71

Slide 71 text

Testing internal details rather than the external API

Slide 72

Slide 72 text

Which one these is best? it('filters the items to only shirts', () => { const shirtFinder = new ShirtFinder({ priceMax: 5000 }) expect(shirtFinder.__foundShirts).toEqual([]) expect(shirtFinder.getShirts()).toEqual([]) })

Slide 73

Slide 73 text

You should be able to rewrite code without changing all your tests.

Slide 74

Slide 74 text

Having consistent test data

Slide 75

Slide 75 text

You'll have a few domain objects that turn up in lots of tests. At Thread, ours are items and item sizes.

Slide 76

Slide 76 text

const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...} const dummyItem = {...}

Slide 77

Slide 77 text

Then, one day: ! All items returned from our API have a new property: 'buttonType' Now you have lots of outdated tests.

Slide 78

Slide 78 text

We can solve this with factories. https://github.com/jackfranklin/test-data-bot

Slide 79

Slide 79 text

export const itemBuilder = build('Item').fields({ brand: fake(f => f.company.companyName()), colour: fake(f => f.commerce.color()), images: { medium: arrayOf(fake(f => f.image.imageUrl()), 3), large: arrayOf(fake(f => f.image.imageUrl()), 3), }, is_thread_own_brand: bool(), name: fake(f => f.commerce.productName()), price: fake(f => parseFloat(f.commerce.price())), sentiment: oneOf('neutral', 'positive', 'negative'), on_sale: bool(), slug: fake(f => f.lorem.slug()), thumbnail: fake(f => f.image.imageUrl()), });

Slide 80

Slide 80 text

import { itemBuilder} from 'frontend/lib/factories' const dummyItem = itemBuilder() const dummyItemWithName = itemBuilder({ name: 'Oxford shirt' })

Slide 81

Slide 81 text

Failing to keep tests isolated

Slide 82

Slide 82 text

Can you spot the problem with these tests? it('has a user when one is logged in', () => { logUserIn() expect(myApp.user).toEqual(dummyUser) }) it('returns a logged in user id', () => { expect(myApp.getLoggedInId()).toEqual(dummyUser.id) })

Slide 83

Slide 83 text

Can you spot the problem with these tests? it('has a user when one is logged in', () => { logUserIn() expect(myApp.user).toEqual(dummyUser) }) it('returns a logged in user id', () => { expect(myApp.getLoggedInId()).toEqual(dummyUser.id) })

Slide 84

Slide 84 text

Each test should run entirely independently of any others it('has a user when one is logged in', () => { logUserIn() expect(myApp.user).toEqual(dummyUser) }) it('returns a logged in user id', () => { logUserIn() expect(myApp.getLoggedInId()).toEqual(dummyUser.id) })

Slide 85

Slide 85 text

This also applies to mocks: make sure you clean up mocks and spies between test runs.

Slide 86

Slide 86 text

Not checking that a test does fail

Slide 87

Slide 87 text

Can you spot the problem with this test? describe('finding items in a price range', () => { it('returns the right set of items', () => { const dummyItems = [{ name: 'shirt', price: 2000 }, ...] const result = itemFinder(dummyItems).min(1000).max(5000) }) })

Slide 88

Slide 88 text

Many test frameworks will pass a test without an assertion!

Slide 89

Slide 89 text

expect.assertions(2)

Slide 90

Slide 90 text

If you write a test and it passes first time, try to break it

Slide 91

Slide 91 text

Why test? How to test? Mistakes in tests Testing React

Slide 92

Slide 92 text

Testing React

Slide 93

Slide 93 text

No content

Slide 94

Slide 94 text

You should buy my course on Testing React! javascriptplayground.com/testing- react-enzyme-jest/ Use ADVANCEDREACT to get 20% off

Slide 95

Slide 95 text

Frameworks for testing React 1: Enzyme 2: react-testing-library

Slide 96

Slide 96 text

I use Enzyme, so the examples here are in Enzyme. react- testing-library is pretty cool though and you should check them both out :)

Slide 97

Slide 97 text

Testing component behaviour

Slide 98

Slide 98 text

it('disappears when closed', () => { const wrapper = shallow(); wrapper.find('.closeIcon').simulate('click'); expect(wrapper.html()).toEqual(null); });

Slide 99

Slide 99 text

These are almost closer to integration tests.

Slide 100

Slide 100 text

Unit test: the shirt finder finds the right shirts Integration test: when I click on the dismiss button, it hides itself

Slide 101

Slide 101 text

We split our frontend tests into three categories: 1: unit tests on "plain JS" modules 2: integration tests on React components 3: end to end tests with Cypress 3 3 which unfortunately I don't have time to cover in this talk

Slide 102

Slide 102 text

How we test our React components

Slide 103

Slide 103 text

First consider: do we need to? For simple components, we might not bother. We prefer plain JS unit tests whenever possible.

Slide 104

Slide 104 text

Test your components as the user

Slide 105

Slide 105 text

No content

Slide 106

Slide 106 text

Which test is better? const wrapper = mount() wrapper.find('a').simulate('click') expect(wrapper.getState().isDisabled).toEqual(true) Or: const wrapper = mount() wrapper.find('button').simulate('click') expect(wrapper.find('button').prop('disabled')).toEqual(true)

Slide 107

Slide 107 text

! Reaches into the component to read some state expect(wrapper.getState().isDisabled).toEqual(true) ! Reads the component as the user would. expect(wrapper.find('button').prop('disabled')).toEqual(true)

Slide 108

Slide 108 text

(react-testing-library only lets you write the second test)

Slide 109

Slide 109 text

Use mocks to set up the component for test Rather than jumping through hoops to "properly" log a user in. import auth from './lib/auth' jest.mock('./lib/auth') test('when the user is logged in it shows their name', () => { auth.isLoggedIn.mockReturnValue(true) ... })

Slide 110

Slide 110 text

Testing UI with snapshots

Slide 111

Slide 111 text

When I render the component with props X, Y, the output should be Z

Slide 112

Slide 112 text

Full snapshots 2 import React from 'react'; import { mount } from 'enzyme'; const HelloWorld = props => (

Hello, {props.name} !

); describe('hello world', () => { it('matches the snapshot', () => { expect(mount()).toMatchSnapshot(); }); }); 2 (https://github.com/adriantoine/enzyme-to-json to enable snapshots on Enzyme wrappers)

Slide 113

Slide 113 text

// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`hello world matches the snapshot 1`] = `

Hello, Jack !

`;

Slide 114

Slide 114 text

The problem with snapshots • Large snapshots = you stop caring about them and the changes: "Oh it's probably fine" • They are in a separate file to the tests - you will miss mistakes in them!

Slide 115

Slide 115 text

! // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`the checkout component renders the card form correctly 1`] = ` null `;

Slide 116

Slide 116 text

Inline Snapshots!

Slide 117

Slide 117 text

No content

Slide 118

Slide 118 text

! the assertion is in the test file, so it's obvious when it's wrong ! you're discouraged from huge snapshots, so you snapshot just the bit you care about

Slide 119

Slide 119 text

Why test? How to test? Mistakes in tests Testing React

Slide 120

Slide 120 text

Fin! • javascriptplayground.com • ADVANCEDREACT for 20% off Testing React and React in 5 courses • Slides will be on speakerdeck.com/jackfranklin

Slide 121

Slide 121 text

If you liked this, you might like... https:// www.youtube.com/ watch?v=z4DNlVlOfjU ...with Kent C. Dodds and myself

Slide 122

Slide 122 text

PS: we're hiring our second frontend engineer at thread: thread.com/jobs or [email protected].

Slide 123

Slide 123 text

Come and find me if you have questions, or tweet @Jack_Franklin

Slide 124

Slide 124 text

No content