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

  2. ! Please write a function to talk to our API

    for finding items within a certain price range
  3. 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(...) }) })
  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(...) }) })
  5. 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 })
  6. 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
  7. ! it('lets me pass in the total items to return',

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

    filtering max price limit is not used
  9. 1. Prove it in a failing test it('filters by max

    price correctly', () => { const items = [{ name: 'shirt', price: 3000 }] expect(itemFinder({ maxPrice: 2000})).toEqual([]) })
  10. This is good! We've recreated and isolated the bug, and

    we can debug without having to manually click around a browser.
  11. 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.
  12. 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.
  13. 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.
  14. Tests let you rewrite, tweak and refactor with a quick

    feedback loop that everything is working as expected.
  15. It's important your application code is well written and maintainable.

    But you can put up with rough edges because it's well tested.
  16. it('clearly says what is being tested', () => { //

    1. Setup // 2. Invoke the code under test // 3. Assert on the results of step 2. })
  17. 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(...) }) })
  18. 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(...) }) })
  19. 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(...) }) })
  20. 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(...) }) })
  21. 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(...) }) })
  22. 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'); });
  23. 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'); }); });
  24. it('finds the value for the given cookie name', () =>

    { document.cookie = 'foo=bar'; const result = getCookie('foo'); expect(result).toEqual('bar'); });
  25. it('fetches the 5 items from our API', async () =>

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

    <[email protected]> Date: Wed Aug 1 17:10:21 2018 +0100 Make items API return first 10, not 5 items
  27. 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) })
  28. 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(..) }) }
  29. 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() })
  30. 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 :)
  31. 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(...) })
  32. beforeEach is a great way to run code before each

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

    result = filterItems(items, 'shirts') expect(result).toEqual(...) })
  34. Fixing this it('filters the items to only shirts', () =>

    { const items = [{ name: 'shirt', ... }, ... ] const result = filterItems(items, 'shirts') expect(result).toEqual(...) })
  35. 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([]) })
  36. You'll have a few domain objects that turn up in

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

    have a new property: 'buttonType' Now you have lots of outdated tests.
  39. 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()), });
  40. import { itemBuilder} from 'frontend/lib/factories' const dummyItem = itemBuilder() const

    dummyItemWithName = itemBuilder({ name: 'Oxford shirt' })
  41. 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) })
  42. 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) })
  43. 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) })
  44. This also applies to mocks: make sure you clean up

    mocks and spies between test runs.
  45. 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) }) })
  46. I use Enzyme, so the examples here are in Enzyme.

    react- testing-library is pretty cool though and you should check them both out :)
  47. it('disappears when closed', () => { const wrapper = shallow(<CookieNotice

    {...props} />); wrapper.find('.closeIcon').simulate('click'); expect(wrapper.html()).toEqual(null); });
  48. Unit test: the shirt finder finds the right shirts Integration

    test: when I click on the dismiss button, it hides itself
  49. 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
  50. First consider: do we need to? For simple components, we

    might not bother. We prefer plain JS unit tests whenever possible.
  51. 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)
  52. ! 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)
  53. 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) ... })
  54. 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)
  55. // 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> `;
  56. 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!
  57. ! 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
  58. Fin! • javascriptplayground.com • ADVANCEDREACT for 20% off Testing React

    and React in 5 courses • Slides will be on speakerdeck.com/jackfranklin