Tip top JavaScript Testing

A presentation at React Berlin in November 2018 in Berlin, Germany by Jack Franklin

Slide 1

Slide 1

! Tip top JavaScript Testing @Jack_Franklin

Slide 2

Slide 2

How to write good tests

Slide 3

Slide 3

Tests are only as useful as the effort you put into them.

Slide 4

Slide 4

It's important your application code is well written and maintainable.

Slide 5

Slide 5

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

Slide 6

Slide 6

Unless you write tests for your tests

Slide 7

Slide 7

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

Slide 8

Slide 8

Slide 9

Slide 9

What makes a great unit test?

Slide 10

Slide 10

Slide 11

Slide 11

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

Slide 12

Slide 12

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 13

Slide 13

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

Slide 14

Slide 14

Tests should have no external dependencies

Slide 15

Slide 15

Keeping unit tests as a unit

Slide 16

Slide 16

If your tests can fail without any of the code you're testing changing, your test is not properly isolated.

Slide 17

Slide 17

This is a huge cause of confusion and frustration in large code bases. ! I changed user.js but tests in blog_posts.js broke

Slide 18

Slide 18

Spies (or mocks, but that doesn't make the picture quite as good)

Slide 19

Slide 19

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

Slide 20

Slide 20

thread-events Logging a user's actions across the site: 1. User 123 clicked on home_feed 2. User 123 added item 456 to the cart 3. User 123 added a new shipping address

Slide 21

Slide 21

import threadEventsLogger from 'thread-events-logger' const processUserClickOnItem = (item) => { threadEventsLogger.log('item_click', { item_id: item.id }) } // does other stuff here too threadEventsLogger: external dependency!

Slide 22

Slide 22

describe('when the user clicks on the item', () => { it('logs a thread-event', () => { // what goes here? }) }) }

Slide 23

Slide 23

Option 1: processUserClickOnItem({ id: 123 }) expect(frontendEventsLog[frontendEventsLog.length - 1]).toEqual({ type: 'item_click', data: { item_id: item.id }, }) Option 2: processUserClickOnItem({ id: 123 }) expect(threadEventsLogger.log).toHaveBeenCalledWith('item_click', { item_id: 123, })

Slide 24

Slide 24

Option 2 relies on threadEventsLogger being thoroughly unit tested itself.

Slide 25

Slide 25

don't test things twice

Slide 26

Slide 26

Avoiding awkward browser interactions in tests with mocks const redirectUser = user => { if (user.authenticated) { window.location.assign('/home') } else if (...) { ... } else { ... } }

Slide 27

Slide 27

test('if the user is logged in they are taken to home', () => { jest.spyOn(window.location, 'assign').mockImplementation(() => {}); redirectUser({ authenticated: true }) expect(window.location.assign).toHaveBeenCalledWith('/home') })

Slide 28

Slide 28

Tidying up after yourself

Slide 29

Slide 29

Mocks won't be automatically cleared between tests test('if the user is logged in they are taken to home', () => { jest.spyOn(window.location, 'assign').mockImplementation(() => {}) redirectUser({ authenticated: true }) expect(window.location.assign).toHaveBeenCalledWith('/home') }) test('if the user is not logged out we do not redirect them', () => { redirectUser({ authenticated: false }) expect(window.location.assign).not.toHaveBeenCalled() }) ‼ the second test is going to fail!

Slide 30

Slide 30

! beforeEach(() => { jest.clearAllMocks() })

Slide 31

Slide 31

Mocks are an essential tool in a developers' testing toolkit

Slide 32

Slide 32

beforeEach

Slide 33

Slide 33

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

Slide 34

Slide 34

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

Slide 35

Slide 35

Where is items coming from?

Slide 36

Slide 36

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

Slide 37

Slide 37

3 parts to a good test it('filters the items to only shirts', () => { const items = [{ name: 'shirt', ... }, ... ] const result = filterItems(items, 'shirts') expect(result).toEqual(...) })

Slide 38

Slide 38

it('...', () => const items = ... }) it('...', () => const items = ... }) it('...', () => const items = ... }) it('...', () => const items = ... }) it('...', () => const items = ... }) it('...', () => const items = ... }) it('...', () => const items = ... }) { [{ name: 'shirt', ... }, ... ] { [{ name: 'shirt', ... }, ... ] { [{ name: 'shirt', ... }, ... ] { [{ name: 'shirt', ... }, ... ] { [{ name: 'shirt', ... }, ... ] { [{ name: 'shirt', ... }, ... ] { [{ name: 'shirt', ... }, ... ]

Slide 39

Slide 39

Functions for test data const getItems = () => [...] it('...', () => { const items = getItems() ... }) ! creation of test data is done for each test ! if the data has to change, we have one place to change it

Slide 40

Slide 40

Having consistent test data It's important that test data resembles your real data.

Slide 41

Slide 41

You'll have a few domain objects that turn up in lots of tests. At Thread we have the Item object.

Slide 42

Slide 42

in every test... const dummyItem = {...}

Slide 43

Slide 43

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

Slide 44

Slide 44

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

Slide 45

Slide 45

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

Slide 46

Slide 46

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

Slide 47

Slide 47

Avoiding brittle tests

Slide 48

Slide 48

When I change the code I have to change the tests as well, so all tests do is double the amount of work I have!

Slide 49

Slide 49

objects have a contract: a public API that they provide

Slide 50

Slide 50

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

Slide 51

Slide 51

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

Slide 52

Slide 52

Test things by calling them just like you do in real life

Slide 53

Slide 53

When writing new tests, check that they fail!

Slide 54

Slide 54

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 55

Slide 55

Many test frameworks will pass a test without an assertion!

Slide 56

Slide 56

expect.assertions(2)

Slide 57

Slide 57

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

Slide 58

Slide 58

small feedback loops

Slide 59

Slide 59

Write code Check if it worked Write code Check if it worked Write code Check if it worked Write code Check if it worked Write code Check if it worked Write code Check if it worked

Slide 60

Slide 60

Write code this time needs to be short Check if it worked

Slide 61

Slide 61

Write code wait 5 seconds for webpack manually refresh the browser click the button you're working on go back to your editor Check if it worked

Slide 62

Slide 62

Write code hit save in your editor Run tests

Slide 63

Slide 63

Fixing bugs with short feedback loops There's a bug where the price filtering max price limit is not used

Slide 64

Slide 64

  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 65

Slide 65

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

Slide 66

Slide 66

  1. Fix the bug without changing the test

Slide 67

Slide 67

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

Slide 68

Slide 68

Confident refactoring

Slide 69

Slide 69

Red Green Refactor

Slide 70

Slide 70

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 71

Slide 71

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 72

Slide 72

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 73

Slide 73

You should feel slightly uncomfortable when you have a failing test.

Slide 74

Slide 74

Testing React

Slide 75

Slide 75

Slide 76

Slide 76

<ShamelessPlug> You should buy my course on Testing React! javascriptplayground.com/testingreact-enzyme-jest/

Slide 77

Slide 77

Use JACKFRIDAY to get 40% off (for today only!)

Slide 78

Slide 78

There is only one rule for testing React components

Slide 79

Slide 79

How can I test hooks in React?

Slide 80

Slide 80

You don't

Slide 81

Slide 81

Slide 82

Slide 82

A React component's contract is what it shows to the user.

Slide 83

Slide 83

So test your components as if you are a user.

Slide 84

Slide 84

Slide 85

Slide 85

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

Slide 86

Slide 86

! 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 87

Slide 87

You can use Enzyme, react-testinglibrary or any alternative. If you test as the user, you'll have good tests. ⁉ The exact framework doesn't actually matter that much.

Slide 88

Slide 88

To conclude...

Slide 89

Slide 89

  1. Remember what makes a good test: setup, invocation, assertion

Slide 90

Slide 90

  1. Avoid brittle tests: test the public contract, not internal details.

Slide 91

Slide 91

  1. When it comes to React, the framework doesn't matter if you test like a user

Slide 92

Slide 92

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

Slide 93

Slide 93

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

Slide 94

Slide 94