Advanced React Meetup: Testing JavaScript

Advanced React Meetup: Testing JavaScript

Aea964cf59c0c81fff752896f070cbbb?s=128

Jack Franklin

October 16, 2018
Tweet

Transcript

  1. Tip top JavaScript Testing @Jack_Franklin

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

  3. Why test?

  4. TDD

  5. None
  6. ! Please write a function to talk to our API

    for finding items within a certain price range
  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(...) }) })
  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(...) }) })
  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 })
  10. TDD lets you test the API before you use it.

  11. But beware!

  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
  13. it('does something but we do not know what yet', ()

    => { })
  14. Here you'll end up writing tests that guess

  15. So you're better off actually writing it

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

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

    minPrice: 5000, ... }) } }
  18. ! it('lets me pass in the total items to return',

    () => { }) it('takes a minPrice option to filter by price', () => { })
  19. Fixing bugs for good There's a bug where the price

    filtering max price limit is not used
  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([]) })
  21. ‼ TEST FAILURE: Expected [], got [ { name: 'shirt',

    price: 3000 }]
  22. This is good! We've recreated and isolated the bug, and

    we can debug without having to manually click around a browser.
  23. 2. Fix the bug without changing the test

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

    []
  25. Confident refactoring

  26. Red Green Refactor

  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.
  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.
  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.
  30. Tests let you rewrite, tweak and refactor with a quick

    feedback loop that everything is working as expected.
  31. Why test? How to test? Mistakes in tests Testing React

  32. How to test

  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.
  34. You don't write tests for your tests So your test

    code should be !
  35. Unless you write tests for your tests

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

    for your tests
  37. What makes a great test?

  38. it('clearly says what is being tested', () => { //

    1. Setup // 2. Invoke the code under test // 3. Assert on the results of step 2. })
  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(...) }) })
  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(...) }) })
  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(...) }) })
  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(...) }) })
  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(...) }) })
  44. You should be able to look at a single it

    test and know everything
  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'); });
  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'); }); });
  47. it('finds the value for the given cookie name', () =>

    { document.cookie = 'foo=bar'; const result = getCookie('foo'); expect(result).toEqual('bar'); });
  48. Tests should have no external dependencies

  49. Network requests

  50. it('fetches the 5 items from our API', async () =>

    { const items = await fetchItems(); expect(items.length).toEqual(5) })
  51. And then one day: commit 81bc0dbedab785ac86be7f8c23e1416db0b99a4e (tag: v19213) Author: Jack

    <jack@thread.com> Date: Wed Aug 1 17:10:21 2018 +0100 Make items API return first 10, not 5 items
  52. If your tests can fail without any of your code

    changing, that is bad.
  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) })
  54. But there's an often cleaner alternative...

  55. Spies and mocks

  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(..) }) }
  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() })
  58. Mocking Fake a function's implementation for the purpose of a

    test.
  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 :)
  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(...) })
  61. Mocks give you a lot of power That you should

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

  63. Mistakes in tests

  64. beforeEach

  65. beforeEach is a great way to run code before each

    test But it can make a test hard to work with or debug.
  66. it('filters the items to only shirts', () => { const

    result = filterItems(items, 'shirts') expect(result).toEqual(...) })
  67. Where is items coming from?

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

    ... }, ... ] })
  69. Fixing this it('filters the items to only shirts', () =>

    { const items = [{ name: 'shirt', ... }, ... ] const result = filterItems(items, 'shirts') expect(result).toEqual(...) })
  70. Keep test set-up close to the test.

  71. Testing internal details rather than the external API

  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([]) })
  73. You should be able to rewrite code without changing all

    your tests.
  74. Having consistent test data

  75. You'll have a few domain objects that turn up in

    lots of tests. At Thread, ours are items and item sizes.
  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 = {...}
  77. Then, one day: ! All items returned from our API

    have a new property: 'buttonType' Now you have lots of outdated tests.
  78. We can solve this with factories. https://github.com/jackfranklin/test-data-bot

  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()), });
  80. import { itemBuilder} from 'frontend/lib/factories' const dummyItem = itemBuilder() const

    dummyItemWithName = itemBuilder({ name: 'Oxford shirt' })
  81. Failing to keep tests isolated

  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) })
  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) })
  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) })
  85. This also applies to mocks: make sure you clean up

    mocks and spies between test runs.
  86. Not checking that a test does fail

  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) }) })
  88. Many test frameworks will pass a test without an assertion!

  89. expect.assertions(2)

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

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

  92. Testing React

  93. None
  94. <ShamelessPlug> You should buy my course on Testing React! javascriptplayground.com/testing-

    react-enzyme-jest/ Use ADVANCEDREACT to get 20% off
  95. Frameworks for testing React 1: Enzyme 2: react-testing-library

  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 :)
  97. Testing component behaviour

  98. it('disappears when closed', () => { const wrapper = shallow(<CookieNotice

    {...props} />); wrapper.find('.closeIcon').simulate('click'); expect(wrapper.html()).toEqual(null); });
  99. These are almost closer to integration tests.

  100. Unit test: the shirt finder finds the right shirts Integration

    test: when I click on the dismiss button, it hides itself
  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
  102. How we test our React components

  103. First consider: do we need to? For simple components, we

    might not bother. We prefer plain JS unit tests whenever possible.
  104. Test your components as the user

  105. None
  106. 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)
  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)
  108. (react-testing-library only lets you write the second test)

  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) ... })
  110. Testing UI with snapshots

  111. When I render the component with props X, Y, the

    output should be Z
  112. Full snapshots 2 import React from 'react'; import { mount

    } from 'enzyme'; const HelloWorld = props => ( <div className="bar"> <h1>Hello, {props.name} !</h1> </div> ); describe('hello world', () => { it('matches the snapshot', () => { expect(mount(<HelloWorld name="Jack" />)).toMatchSnapshot(); }); }); 2 (https://github.com/adriantoine/enzyme-to-json to enable snapshots on Enzyme wrappers)
  113. // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`hello world matches the snapshot

    1`] = ` <HelloWorld name="Jack" > <div className="bar" > <h1> Hello, Jack ! </h1> </div> </HelloWorld> `;
  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!
  115. ! // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`the checkout component renders

    the card form correctly 1`] = ` null `;
  116. Inline Snapshots!

  117. None
  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
  119. Why test? How to test? Mistakes in tests Testing React

  120. Fin! • javascriptplayground.com • ADVANCEDREACT for 20% off Testing React

    and React in 5 courses • Slides will be on speakerdeck.com/jackfranklin
  121. If you liked this, you might like... https:// www.youtube.com/ watch?v=z4DNlVlOfjU

    ...with Kent C. Dodds and myself
  122. PS: we're hiring our second frontend engineer at thread: thread.com/jobs

    or jack@thread.com.
  123. Come and find me if you have questions, or tweet

    @Jack_Franklin
  124. None