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 full-size slide

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

    View full-size slide

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

    View full-size slide

  4. 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 full-size slide

  5. 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 full-size slide

  6. 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 full-size slide

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

    View full-size slide

  8. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  11. So you're better
    off actually writing
    it

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. 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 full-size slide


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

    View full-size slide

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

    View full-size slide

  19. 2. Fix the bug
    without changing
    the test

    View full-size slide

  20. 3. Rerun the test

    TEST PASSED
    Expected [], got []

    View full-size slide

  21. Confident
    refactoring

    View full-size slide

  22. Red
    Green
    Refactor

    View full-size slide

  23. 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 full-size slide

  24. 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 full-size slide

  25. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  28. 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 full-size slide

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

    View full-size slide

  30. Unless you write
    tests for your tests

    View full-size slide

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

    View full-size slide

  32. What makes a great test?

    View full-size slide

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

    View full-size slide

  34. 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 full-size slide

  35. 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 full-size slide

  36. 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 full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

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

    View full-size slide

  40. 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 full-size slide

  41. 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 full-size slide

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

    View full-size slide

  43. Tests should have
    no external
    dependencies

    View full-size slide

  44. Network requests

    View full-size slide

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

    View full-size slide

  46. 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 full-size slide

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

    View full-size slide

  48. 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 full-size slide

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

    View full-size slide

  50. Spies and mocks

    View full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

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

    View full-size slide

  54. 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 full-size slide

  55. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  58. Mistakes in tests

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  61. Where is items
    coming from?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  65. Testing internal
    details rather than
    the external API

    View full-size slide

  66. 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 full-size slide

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

    View full-size slide

  68. Having consistent
    test data

    View full-size slide

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

    View full-size slide

  70. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  73. 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 full-size slide

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

    View full-size slide

  75. Failing to keep
    tests isolated

    View full-size slide

  76. 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 full-size slide

  77. 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 full-size slide

  78. 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 full-size slide

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

    View full-size slide

  80. Not checking that a test does fail

    View full-size slide

  81. 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 full-size slide

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

    View full-size slide

  83. expect.assertions(2)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  86. Testing React

    View full-size slide


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

    View full-size slide

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

    View full-size slide

  89. 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 full-size slide

  90. Testing
    component
    behaviour

    View full-size slide

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

    View full-size slide

  92. These are almost
    closer to
    integration tests.

    View full-size slide

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

    View full-size slide

  94. 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 full-size slide

  95. How we test our React
    components

    View full-size slide

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

    View full-size slide

  97. Test your
    components as
    the user

    View full-size slide

  98. 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 full-size slide

  99. !
    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 full-size slide

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

    View full-size slide

  101. 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 full-size slide

  102. Testing UI with
    snapshots

    View full-size slide

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

    View full-size slide

  104. 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 full-size slide

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

    Hello,
    Jack
    !



    `;

    View full-size slide

  106. 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 full-size slide

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

    View full-size slide

  108. Inline Snapshots!

    View full-size slide

  109. !
    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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide