Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Advanced React Meetup: Testing JavaScript

Advanced React Meetup: Testing JavaScript

Jack Franklin

October 16, 2018
Tweet

More Decks by Jack Franklin

Other Decks in Technology

Transcript

  1. Tip top JavaScript
    Testing
    @Jack_Franklin

    View Slide

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

    View Slide

  3. Why test?

    View Slide

  4. TDD

    View Slide

  5. View Slide

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

    View Slide

  7. 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(...)
    })
    })

    View Slide

  8. 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(...)
    })
    })

    View Slide

  9. 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 })

    View Slide

  10. TDD lets you test
    the API before you
    use it.

    View Slide

  11. But beware!

    View Slide

  12. 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

    View Slide

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

    View Slide

  14. Here you'll end up
    writing tests that
    guess

    View Slide

  15. So you're better
    off actually writing
    it

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide


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

    View Slide

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

    View Slide

  23. 2. Fix the bug
    without changing
    the test

    View Slide

  24. 3. Rerun the test

    TEST PASSED
    Expected [], got []

    View Slide

  25. Confident
    refactoring

    View Slide

  26. Red
    Green
    Refactor

    View Slide

  27. 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.

    View Slide

  28. 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.

    View Slide

  29. 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.

    View Slide

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

    View Slide

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

    View Slide

  32. How to test

    View Slide

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

    View Slide

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

    View Slide

  35. Unless you write
    tests for your tests

    View Slide

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

    View Slide

  37. What makes a great test?

    View Slide

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

    View Slide

  39. 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(...)
    })
    })

    View Slide

  40. 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(...)
    })
    })

    View Slide

  41. 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(...)
    })
    })

    View Slide

  42. 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(...)
    })
    })

    View Slide

  43. 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(...)
    })
    })

    View Slide

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

    View Slide

  45. 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');
    });

    View Slide

  46. 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');
    });
    });

    View Slide

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

    View Slide

  48. Tests should have
    no external
    dependencies

    View Slide

  49. Network requests

    View Slide

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

    View Slide

  51. 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

    View Slide

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

    View Slide

  53. 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)
    })

    View Slide

  54. But there's an
    often cleaner
    alternative...

    View Slide

  55. Spies and mocks

    View Slide

  56. 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(..)
    })
    }

    View Slide

  57. 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()
    })

    View Slide

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

    View Slide

  59. 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 :)

    View Slide

  60. 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(...)
    })

    View Slide

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

    View Slide

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

    View Slide

  63. Mistakes in tests

    View Slide

  64. beforeEach

    View Slide

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

    View Slide

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

    View Slide

  67. Where is items
    coming from?

    View Slide

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

    View Slide

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

    View Slide

  70. Keep test set-up
    close to the test.

    View Slide

  71. Testing internal
    details rather than
    the external API

    View Slide

  72. 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([])
    })

    View Slide

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

    View Slide

  74. Having consistent
    test data

    View Slide

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

    View Slide

  76. 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 = {...}

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  81. Failing to keep
    tests isolated

    View Slide

  82. 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)
    })

    View Slide

  83. 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)
    })

    View Slide

  84. 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)
    })

    View Slide

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

    View Slide

  86. Not checking that a test does fail

    View Slide

  87. 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)
    })
    })

    View Slide

  88. Many test
    frameworks will
    pass a test without
    an assertion!

    View Slide

  89. expect.assertions(2)

    View Slide

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

    View Slide

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

    View Slide

  92. Testing React

    View Slide

  93. View Slide


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

    View Slide

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

    View Slide

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

    View Slide

  97. Testing
    component
    behaviour

    View Slide

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

    View Slide

  99. These are almost
    closer to
    integration tests.

    View Slide

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

    View Slide

  101. 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

    View Slide

  102. How we test our React
    components

    View Slide

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

    View Slide

  104. Test your
    components as
    the user

    View Slide

  105. View Slide

  106. 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)

    View Slide

  107. !
    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)

    View Slide

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

    View Slide

  109. 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)
    ...
    })

    View Slide

  110. Testing UI with
    snapshots

    View Slide

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

    View Slide

  112. 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)

    View Slide

  113. // Jest Snapshot v1, https://goo.gl/fbAQLP
    exports[`hello world matches the snapshot 1`] = `
    name="Jack"
    >
    className="bar"
    >

    Hello,
    Jack
    !



    `;

    View Slide

  114. 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!

    View Slide

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

    View Slide

  116. Inline Snapshots!

    View Slide

  117. View Slide

  118. !
    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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  124. View Slide