TESTING REACT COMPONENTS

Tooling

Sinon.JS

Enzyme

Jest

React

different kinds of

rendering

shallow rendering

  • Renders root component and it's native children (like <div>) as deep as necessary
     
  • Does not render subcomponents meaning parent component  tests do not depend on children working properly
     
  • Does not run all lifecycle methods (like componentDidMount)
     
  • Much faster than full mount

full dom rendering

  • Renders entire tree
     
  • Requires browser or browser-like environment (JSDOM) to have access to DOM API
     
  • Runs all lifecycle methods
     
  • Can be used to test integrations with external libraries that modify DOM outside of React
     
  • Much closer to what's actually in browser than shallow rendering but at the cost of speed and less isolation

static html rendering

  • Renders entire tree to static HTML
     
  • Uses Cheerio.js library
     
  • Useful for testing the validity of resulting HTML and not the individual components implementations
     
  • Components that were mounted or shallow rendered can still use this by calling .render() (returns CheerioWrapper)

Golden rules of testing REACT COMPONENTS

  • test one thing at a time to avoid coupling your tests and making them easier to break
     
  • use shallow rendering for unit tests of simple components and full mount for integration tests of complex components
     
  • DO. NOT. TEST. WRAPPED. COMPONENTS.

unwrapping

components

method 1

import React from 'react'
import { shallow } from 'enzyme'
import { inject } from 'mobx-react'

const Button = inject('store')(({ store }) => {
  return <button>{store.label}</button>
})

describe('Button', () => {
  it('renders with label', () => {

    // notice the .dive() here
    const component = shallow(<Button store={{ label: 'foo' }}/>).dive()

    expect(component.find('button').text()).toEqual('foo')
  })
})

use .dive() to render inside of HoC

PROS

  • does not require any additional work

CONS

  • practically everything else
     
  • can be unpredictable and throw arcane errors
     
  • does not respect context (will not work with react router properly)

method 2

import React from 'react'
import { shallow } from 'enzyme'
import { inject } from 'mobx-react'
import { withRouter } from 'react-router-dom'

const Button = inject('store')(withRouter(({ store }) => {
  return <button>{store.label}</button>
}))

describe('Button', () => {
  it('renders with label', () => {

    // You can already see the problem...
    const TestedButton = Button.wrappedComponent.WrappedComponent

    const component = shallow(<TestedButton store={{ label: 'foo' }}/>)
    expect(component.find('button').text()).toEqual('foo')
  })
})

use escape hatches

PROS

  • it's there
     
  • library authors recommend this (?)

CONS

  • forces to remember about how many HoC were applied to component
     
  • forces to remember about unwrapping everything in tests or it will break
     
  • adding new HoC requires changing all tests
     
  • need to remember how escape hatch is implemented (wrappedComponent for mobx-react but WrappedComponent for react-router etc.) and in which order they were applied

method 3

// components/button.js
import React from 'react'
import { inject } from 'mobx-react'
import { withRouter } from 'react-router-dom'

const Button = ({ store }) => {
  return <button>{store.label}</button>
}

export { Button }
export default inject('store')(withRouter(Button))

export pure class from file

// components/button.test.js
import React from 'react'
import { shallow } from 'enzyme'
import { Button } from 'components/button'

describe('Button', () => {
  it('renders with label', () => {
    const component = shallow(<Button store={{ label: 'foo' }}/>)
    expect(component.find('button').text()).toEqual('foo')
  })
})

PROS

  • clean separation between class and it's decorators
     
  • super easy to use
     
  • makes tests predictable

CONS

  • slightly more verbose export
     
  • cannot use @decorator for entire class if it creates a HoC (withRouter, inject)
     
  • no-named-as-default eslint rule - can be avoided with aliasing, e.g.
    export { Foo as BaseFoo }

types of tests

snapshot tests

snapshot tests

  • requires extra library to work seamlessly with Enzyme (enzyme-to-json)
     
  • slowest of all
     
  • generates JSON and compares it with the saved one on the hard drive
     
  • needs to be extra careful around passed props to avoid huge JSON files (always mock everything)

snapshot tests

  • useful when testing a larger block of HTML to make sure it still works correctly
     
  • not very precise so those tests break most often (any change to resulting HTML changes the tests)
     
  • more of a sanity check than an actual test and should be treated as such (instead of as an easy mean of artificially increasing the test coverage)
     
  • useful when doing Red/Green/Refactor cycle as the diff between rendered and saved snapshot shows exactly where the test failed

snapshot tests

import React from 'react'
import { shallow } from 'enzyme'
import snapshot from 'enzyme-to-json'

const Button = ({ store }) => {
  return <button>{store.label}</button>
}

describe('Button', () => {
  it('matches the snapshot', () => {
    const component = shallow(<Button store={{ label: 'foo' }}/>)

    // this line is important
    expect(snapshot(component)).toMatchSnapshot()
  })
})

types of tests

rendering tests

rendering tests

  • just render the component (either with shallow rendering, full mount or static HTML) and test if things are OK
     
  • mostly focused on mocking props (ones that are not functions) and checking the result
     
  • faster than snapshot tests but require manual comparisons
     
  • more precise than snapshot testing, less brittle and provides better insight into what exactly changed and how

rendering tests

import React from 'react'
import { shallow } from 'enzyme'
import toJson from 'enzyme-to-json'

const Button = ({ store }) => {
  return <button>{store.label}</button>
}

describe('Button', () => {
  it('renders label', () => {
    const component = shallow(<Button store={{ label: 'foo' }}/>)
    expect(component.find('button').text()).toEqual('foo')
  })
})

types of tests

behaviour tests

behavior tests

  • simulate platform (browser, native etc.) events and test component behaviors
     
  • mostly focused on mocking props (ones that are functions) and checking if event handlers are attached properly
     
  • often side effects or calls outside should be mocked/stubbed to avoid test coupling

bEHAVIOR TEST

class EmptyForm extends React.Component {
  componentWillMount() {
    this.form = new Form()
  }

  onSubmit(e) {
    this.form.onSuccess(e)
  }

  render() {
    return (
      <form onSubmit={this.onSubmit.bind(this)}>
        <button type="submit">Submit</button>
      </form>
    )
  }
}
const sandbox = sinon.createSandbox()

describe('EmptyForm', () => {
  afterAll(() => sandbox.restore())

  it('triggers form object onSuccess on submit', () => {
    const component = shallow(<EmptyForm/>)
    const instance = component.instance()
    const stub = sandbox.stub(instance.form, 'onSuccess')
                        .callsFake(() => true)

    component.find('form').simulate('submit')
    expect(stub.calledOnce).toBe(true)
  })
})

enzyme does not support event bubbling and does not truly fake browser events, it simply runs the event handler from props

CAVEAT

component.simulate('event', { extraData })

// is the same as

component.prop('onEvent')({ extraData })

// so these are equivalent:

component.simulate('click')
component.prop('onClick')()

// if you need to test actual event bubbling, you
// need to fake it yourself :(

types of tests

instance methods tests

instance method tests

  • either render component or instantiate store, form object etc. and then test it's instance methods
     
  • useful for testing message passing (for ex. checking if form object is called properly)
     
  • useful for testing logic in isolation
     
  • most of the time you shouldn't write those tests for React components (only form objects, stores etc.) and use behaviour tests instead

instance method tests

class TotalPrice extends React.Component {
  calculateTotal() {
    return this.props.items.reduce(
      (sum, item) => sum + (item.price || 0)
    , 0)
  }

  render() {
    return (
      <div>
        Total price: {this.calculateTotal()}
      </div>
    )
  }
}
describe('TotalPrice', () => {
  describe('calculateTotal', () => {
    it('sums all items', () => {
      const items = [ { price: 10 }, { price: 15 } ]
      const component = shallow(<TotalPrice items={items}/>)
      const instance = component.instance()
      expect(instance.calculateTotal()).toEqual(25)
    })

    it('works with empty list', () => {
      const component = shallow(<TotalPrice items={[]}/>)
      const instance = component.instance()
      expect(() => {
        expect(instance.calculateTotal()).toEqual(0)      
      }).not.toThrow()
    })

    it('works with items with no price', () => {
      const component = shallow(<TotalPrice items={[{}, {}]}/>)
      const instance = component.instance()
      expect(() => {
        expect(instance.calculateTotal()).toEqual(0)      
      }).not.toThrow()
    })
  })
})

types of tests

integration tests

integration tests

  • testing of components that mount other components
     
  • should always do full mount
     
  • These kinds of tests are basically behaviour tests for multiple components at the same time
     
  • No example, couldn't find a way to simplify it for the slides (sorry!)

types of tests

system tests

system tests

  • Very similar to integration tests but run in a browser (or other platform like Android/iOS) instead of simulated environment
     
  • Take longest to run so they should be launched from separate pipeline (and only on CI, not for ex. as pre-commit hook)
     
  • Require browser (can be in headless mode) and running web server
     
  • Actually harder to do in Node.js than in Rails due to tooling being less mature...?

system tests

  • The biggest problem when running system tests is that you either need to spawn your API server or mock everything and keep those mocks in sync
     
  • It is preferable to run those tests on production build but with test data
     
  • If you're using Apollo Client, it's default behaviour is to return null when server response does not match schema so you can abuse this behaviour to make sure your mocks are up to date (not a perfect solution though)

system tests

Nightmare

types of tests

visual regression tests

visual regression tests

  • Like system tests, these are also required to run in browser
     
  • Something between snapshot and system tests
     
  • Opens a browser, takes a screenshot, compares it with previously saved screenshot and throws if they're not identical
     
  • Only useful for stable parts of the application
     
  • They can be VERY frustrating with various environments due to rendering differences (mostly font rendering)

visual regression tests

chromeless + jest-image-snapshot

jest

configuration

configuration

  • Jest is a testing framework made by Facebook. It's a fork of Jasmine preloaded with useful plugins and with sane default configuration.
     
  • Most of the time to start testing  you just have to add jest package to the project and add script to package.json running jest binary.
     
  • It's automatically preconfigured to look for tests both in __tests__ directory and for *.test.js files in entire project (except node_modules, obviously)

configuration

Sometimes you need to set some things up before testing (like Enzyme adapter). You can create a test.config.js file and put this code there.

{
  "name": "react-testing-workshop",
  "version": "0.1.0",
  // ...
  "jest": {
    "setupFilesAfterEnv": 
      "<rootDir>/test.config.js"
  }
}
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-15';

configure({ adapter: new Adapter() });

add this to your package.json

now you can configure enzyme adapter

If you're using create-react-app, the file already exists as setupTests.js

how to write tests

jest basics

jest basics

  • Jest uses rspec-like syntax for writing tests.
     
  • Three global methods available in every tests: describe(), it() and expect(). It() is also aliased as test() so don't get surprised if you see it in docs.
     
  • You group your tests using describe() block and then use it() blocks to write specific tests using expect() and matchers.
     
  • If it() block returns Promise, Jest waits for it to resolve.
    This means we can use async/await for easier async testing.

example

describe('User', () => {
  it('returns username', () => {
    const user = new User('username')
    expect(user.username).toBe('username')
  })

  // Asynchronous tests are easy with async/await
  it('fetches user', async () => {
    const user = await User.fetch()
    expect(user).toEqual({ username: 'Foo' })
  })
})

example

describe('refreshAccessToken', () => {
  it('requests access token via JSON request to API', () => {
    expect.assertions(4)

    sandbox.stub(global, 'fetch').callsFake(async (url, args) => {
      expect(url).toBe(oauthTokenPath())
      expect(args.method).toEqual('POST')
      expect(args.headers).toMatchObject({ 'Content-Type': 'application/json' })
      expect(JSON.parse(args.body)).toEqual({
        refresh_token: 'REFRESH_ME',
        grant_type: 'refresh_token'
      })

      return successResponse
    })
    return refreshAccessToken('REFRESH_ME')
  })
})

If you're testing complex things that you don't have complete control over but you still want to make sure all expectations are ran and accounted for, you can use expect.assertions()

how to write tests

jest matchers

jest matchers

When you require strict comparison, use toBe

Examples: true/false, numbers, strings

describe('calculator', () => {
  it('adds numbers', () => {
    expect(calculator.add(2, 2)).toBe(4)
    // there is also .not
    expect(calculator.add(2, 2)).not.toBe(5)
  }
})

jest matchers

When you require value comparison, use toEqual

Examples: arrays, objects (tested recursively)

describe('sign in', () => {
  it('returns user', () => {
    expect(signIn('user', 'pass')).toEqual({ username: 'user' })
    expect(signIn('user', 'pass')).not.toEqual({ username: '1' })
  }
})

jest matchers

To compare a value (serializable object) with pre-existing snapshot,
use toMatchSnapshot. You'll often need an extra lib (like enzyme-to-json)

it('matches snapshot', () => {
  const component = shallow(<Foo/>)
  expect(toJson(component)).toMatchSnapshot()
}

jest matchers

There are also matchers for null, undefined, defined (!== undefined)
and truthy/falsy values (treated by if() as true or false)

it('showcases all matchers :P', () => {
  const n = null;
  expect(n).toBeNull();
  expect(n).toBeDefined();
  expect(n).not.toBeUndefined();
  expect(n).not.toBeTruthy();
  expect(n).toBeFalsy();
})

jest matchers

There are matchers for numbers. The most useful is toBeCloseTo because it works around issues with floating point precision.

it('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});

it('adding floating point numbers', () => {
  const value = 0.1 + 0.2;
  expect(value).not.toBe(0.3);    // It isn't! Because rounding error
  expect(value).toBeCloseTo(0.3); // This works.
});

jest matchers

To match strings according to regular expression,

use toMatch

it('returns correct error message', () => {
  expect(form.error).toMatch(/does not exist/)
})

jest matchers

To check if element is part of an array, use toContain

it('does reality check', () => {
  expect(edibleFood).toContain('meat')
})

jest matchers

Finally, to check if a block of code does (or doesn't) throw an exception, use toThrow

it('crashes with invalid data', () => {
  expect(() => {
    signIn()
  }).toThrow()
})

it('crashes with specific error', () => {
  expect(() => {
    signIn()
  }).toThrow(/specific error/)
})

jest matchers - summary

  • toBe - immutable objects, strict comparison
  • toEqual - value comparison
  • toMatchSnapshot
  • toBeGreaterThan
  • toBeGreaterThanOrEqual
  • toBeLessThan
  • toBeLessThanOrEqual
  • toBeCloseTo - for numbers with fraction part
  • toBeNull - checks if value is null
  • toBeUndefined - checks if value is undefined
  • toBeDefined - checks if value is NOT undefined
  • toBeTruthy - if(value)
  • toBeFalsy - if(!value)
  • toMatch - matches substring (or regex)
  • toContain - matches if array contains element
  • toThrow - matches if block of code threw an error

too much?

IN MOST OF THE TESTS YOU ARE GOING TO USE ONLY TOBE, TOEQUAL AND TOTHROW ANYWAY

For the rest just keep this tab open:

https://facebook.github.io/jest/docs/en/using-matchers.html

how to write tests

spies, stubs, mocks

spies, stubs, mocks

  • For all our stubbing and spying  needs we use Sinon.js
     
  • Jest also implements mocks but Sinon has nicer API and more features

spies

  • Spy is a method that keeps track of how it was called
     
  • It's the simplest of the three (spy, stub, mock) as it doesn't give you control over return value
     
  • Useful to replace some operations with noops just to make sure they were called (events, method calls that ignore return value etc.)

spies

it('redirects to root path after saving', () => {
  // mocking minimal react-router history API
  const history = { push: sinon.spy() }

  const component = shallow(<Foo history={history}/>)

  const instance = component.instance()
  instance.onSave()

  expect(history.push.calledWith('/')).toBe(true)
})

stubs

  • Stubs are more complex spies.
     
  • Instead of only tracking when they're called, stubs can also return values, call original function (or wrap it), throw errors and overall be a test-specific replacement for a function.
     
  • They are most useful when disabling parts of the application we do not want to test at the moment (XHR, complex operations, side effects etc) or when we want to make sure a certain path in code is being run (like making sure an if() somewhere gets true/false or when triggering error handling)

stubs

it('throws if user does not exist', () => {
  const userStore = new UserStore(1)
  sinon.stub(userStore, 'fetchUser')
       .callsFake(() => null)

  expect(() => {
    userStore.getUserProfile()
  }).toThrow(/does not exist/)
})

mocks

  • Mocks are basically stubs with their own expectations.
     
  • They are optional if you are ok with specifying the behaviour later rather than on initialization.
     
  • They have their own .verify() method that throws if any of the expectations are not met so it doesn't work that well with Jest.
     
  • They are used to wrap entire objects.

mocks

it('fetches user from server', () => {
  const mock = sinon.mock(Api);
  mock.expects("fetch").once().withArgs(1);

  const store = new UserStore
  store.getUser(1)

  mock.verify();
})

wait, what about

test isolation?!

sandbox

Sinon implements a Sandbox mechanism which allows you to

easily restore the state in global objects between tests.

const sandbox = sinon.createSandbox()

describe('User', () => {
  afterEach(() => sandbox.restore())

  it('does something', () => {
    // use sandbox.foo instead of sinon.foo
    sandbox.spy()
    sandbox.stub()
    sandbox.mock()
  })
})

and how do I

mock dependencies

mocking dependencies

  • For mocking dependencies you cannot use sinon
     
  • This is the only case where jest.mock() have to come to play
     
  • You can mock either module dependencies or even local files if needed. This is useful for ex. when building React Native app if you want to mock native parts or when you want to stub external functions without introducing complex dependency injection
     
  • You can use jest.requireActual to escape the mocking

EXAMPLE

jest.mock('react-native-web', () => ({
  ...jest.requireActual('react-native-web'),
  InteractionManager: {
    runAfterInteractions: cb => cb()
  }
}))

// now we don't have to worry in our tests about
// API calls being scheduled asynchronously as a
// workaround for performance issues on Android

how to write tests

fake timers

fake timers

  • Some of the code you are testing may use setTimeout, setInterval or Date.
     
  • To be able to control when those callbacks trigger, sinon gives you ability to use fake timers. They are reimplementations of time-related functions with controllable clock.
     
  • Even if your own code does not use them, some of the libraries are using setTimeout(0) to run callbacks after main function has been processed.

fake timers

class Timer extends React.Component {
  constructor(props) {
    super(props)
    this.state = { value: 0 }
    this.tick = this.tick.bind(this)
  }

  componentWillMount() {
    this.timer = setInterval(this.tick, 1000)
  }

  tick() {
    this.setState({ value: this.state.value + 1 })
  }

  comopnentWillUnmount() {
    removeInterval(this.timer)
  }

  render() {
    return (
      <span className="value">
        {this.state.value}
      </span>
    )
  }
}
let clock

describe('Counter', () => {
  beforeEach(() => 
    clock = sinon.useFakeTimers()
  )
  afterEach(() => clock.restore())

  it('updates after time has passed', () => {
    const component = shallow(<Timer/>)

    // Advance clock 1000 ms
    clock.tick(1000)

    component.update()
    const value = component.find('.value').text()
    expect(value).toMatch("1")
  })
})

how to write tests

stubbing ajax requests

stubbing ajax requests

xmlhttprequest

xmlhttprequest

The most universal way if you're using XMLHttpRequest (either directly or via library like $.ajax) is to use Sinon's fake XHR and server

class UserProfile extends React.Component {
  constructor(props) { 
    super(props); 
    this.state = {} 
  }
  componentWillMount() {
    $.ajax('/api/users/' + this.props.id)
      .then((user) => this.setState({ user }))
  }
  render() {
    const { user } = this.state
    if(user) { 
      return <span>User name: {user.name}</span>
    } else {
      return <span>Loading...</span>
    }
  }
}
const sandbox = sinon.createSandbox()
let fakeXHR, requests

describe('UserProfile', () => {
  beforeEach(() => {
    fakeXHR = sinon.useFakeXMLHttpRequest()
    requests = []
    fakeXHR.onCreate = req => requests.push(req)
  })
  afterEach(() => {
    fakeXHR.restore()
    requests.length = 0
  })

  it('fetches user data', () => {
    const component = shallow(<UserProfile id="1"/>)
    expect(requests.length).toBe(1)
    expect(requests[0].url).toEqual("/api/users/1")
  })
})

xmlhttprequest

CAREFUL: you may be inclined to write a test like this. It will not work, because jQuery resolve promises/deferreds asynchronously.

it('shows user name', () => {
  const component = shallow(<UserProfile id="1"/>)
  requests[0].respond(200, 
    {'Content-type': 'application/json'}, 
    JSON.stringify({ name: 'Hello' })
  )
  component.update()
  // THIS WILL FAIL, the callback did not run yet
  // and the state is not yet set
  expect(component.text()).toMatch(/Hello/)
})

xmlhttprequest

You can make sure all of the callbacks have run by using fake timers.

This is not an ideal solution as it relies on an implementation detail.

it('shows user name', () => {
  const component = shallow(<UserProfile id="1"/>)
  requests[0].respond(200, 
    {"Content-type": "application/json"}, 
    JSON.stringify({ name: "Hello" })
  )
  // run all setTimeout(0) callbacks
  clock.runAll()

  component.update()
  expect(component.text()).toMatch(/Hello/)
})

xmlhttprequest

Expecting requests to always come in the same order may lead to brittle tests. Sinon provides fake XHR server to keep track of requests.

let server, clock
describe('SignUpPage', () => {
  beforeAll(() => {
    server = sinon.createFakeServer()
    clock  = sinon.useFakeTimers()
  })
  afterAll(() => {
    server.restore()
    clock.restore()
  })
  it('fetches user data', async () => {
    server.respondWith("GET", "/api/users/1", [
      200, { "Content-Type": "application/json" }, JSON.stringify({ name: "Hello" })
    ])
    const component = shallow(<UserProfile id="1"/>)
    server.respond()
    clock.runAll()
    component.update()
    expect(component.text()).toMatch("Hello")
  })
})

stubbing ajax requests

axios

AXIOS

  • Due to the way axios is written, using Sinon's fake server can be a bit cumbersome.
     
  • We are going to use axios-mock-adapter instead.
     
  • Our previous trick with fake timers only work for some libraries depending on their internal implementation. Axios is not one of those libraries.
     
  • The simplest solution: use delay()

AXIOS

To wait in your test, use delay library. It returns a Promise that resolves itself after N ms. Be careful: Sinon fake timers break this.

class UserProfile extends React.Component {
  constructor(props) {
    super(props)
    this.state = {}
  }
  componentWillMount() {
    axios.get('/api/users/' + this.props.id)
      .then(({ data }) => 
        this.setState({ user: data })
      )
  }
  render() {
    const { user } = this.state
    if(user) { 
      return <span>User name: {user.name}</span>
    } else {
      return <span>Loading...</span>
    }
  }
}
let mock

describe('UserProfile', () => {
  beforeEach(() => mock = new MockAdapter(axios))
  afterEach(() => mock.restore())

  it('fetches user data', async () => {
    mock.onGet("/api/users/1").reply(200,
      { name: "Hello" }
    )

    const component = shallow(<UserProfile id="1"/>)
    await delay(0)
    component.update()
    expect(component.text()).toMatch("Hello")
  })
})

Why delay(0) ? Because of how event loop in node works, see: https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

axios

axios-mock-adapter  supports multiple ways of specifying
on what exactly to react

// onGet, onPost, onPatch etc.

mock.onGet('/url')
mock.onGet('/url', { params })
mock.onGet(/regexp/)
mock.onGet() // matches all GET requests

mock.onAny() // all types of requests

axios

axios-mock-adapter  supports multiple ways of specifying
on how exactly to react

mock.onGet('/url').reply(200) // HTTP 200, no response
mock.onGet('/url').reply(200, {}) // JSON

// replyOnce unpins mock after first matching request
mock.onGet('/url').replyOnce(200, "abc")

mock.onGet('/url').reply((config) => {
  // entire request is available
  // e.g. config.url, config.method 
  return [200, {}]
})

axios

Keep in mind that the order of mocks matter (they're evaluated from oldest to newest). Axios-mock-adapter also supports method chaining.

mock.onGet('/url').reply(200)
    .onPost('/url').reply(200, {}) 

// last onAny can be used as catch-all

mock.onAny().reply((config) => {
  throw "Unmocked request to ${config.url}!!!"
})

axios

axios-mock-adapter also allows us to
fake errors, timeouts and network errors

mock.onGet('/url').reply(404) // HTTP 404
mock.onGet('/url').reply(403, {error: "Nope"})

// low-level network error
mock.onGet('/url').networkError()

// timeout (ECONNABORTED)
mock.onGet('/url').timeout()

axios

By default, axios-mock-adapter returns 404 for non-mocked requests.
If you want to actually let a request go through, use passThrough

mock.onGet(/google\.com/).passThrough()

// TRICK: remember about "last onAny"?
// if you want non-mocked requests to
// go through instead of fail with 404:

mock.onAny().passThrough()

stubbing ajax requests

fetch api

fetch api

  • We are going to use fetch-mock for mocking fetch API.
     
  • Remember that Node.js does not provide fetch() by default. You need to use isomorphic-fetch or other polyfill.
     
  • The idea is similar to previous examples so I'm not going to go into details. Consult documentation to see the differences in API.

fetch api

As you can see the general idea and feature set are

very similar to previous libraries.

class UserProfile extends React.Component {
  constructor(props) {
    super(props)
    this.state = {}
  }
  componentWillMount() {
    fetch('/api/users/' + this.props.id)
      .then(async (response) => {
        const user = await response.json()
        this.setState({ user })
      })
  }
  render() {
    const { user } = this.state
    if(user) { 
      return <span>User name: {user.name}</span>
    } else {
      return <span>Loading...</span>
    }
  }
}
describe('SignUpPage', () => {
  afterEach(() => fetchMock.restore())

  it('fetches user data', async () => {
    fetchMock.get("/api/users/1", { name: "Hello" })

    const component = shallow(<UserProfile id="1"/>)
    await delay(10)
    component.update()
    expect(component.text()).toMatch("Hello")
  })
})

a word of caution

  • These were all artificial examples designed to show how you can mock HTTP requests
     
  • In a real life scenario you should not schedule XHR requests inside your components this way and instead push and test them elsewhere (for ex. as separate functions, props passed from the store etc)
     
  • The delay() thing is totally a hack and should only be used if all other methods have failed (sorry for bad examples!)

case studies

video quality picker

import React, { Component } from 'react'
import { observable, action } from 'mobx'
import { observer, inject } from 'mobx-react'
import classNames from 'classnames/bind'
import Icon from 'fa/icon'
import styles from './index.scss'
const cx = classNames.bind(styles)

@observer
class Quality extends Component {
  @observable visible = false
  
  @action onToggle() {
    this.visible = !this.visible
  }
  
  @action setStream(stream) {
    this.props.store.setCurrentStream(stream)
    this.visible = false
  }
  
  render() {
    const { store } = this.props
    const className = cx({
      'quality-picker__dropdown': true,
      'quality-picker__dropdown--visible': this.visible
    })
    
    if(!store.isReady) {
      return (
        <div className={cx('button')}><Icon name="cog"/></div>
      )
    }
    
    return (
      <div>
        <div className={cx('button', 'quality-picker')} onClick={this.onToggle.bind(this)}>
          {store.currentStream && store.currentStream.shortLabel}
        </div>
        <div>
          <div className={className}>
            {store.video.getVideoStreams().map((stream) => (
              <div className={cx('quality-picker__item')}
                   onClick={this.setStream.bind(this, stream)}
                   key={stream.format}>
                {stream.label}
              </div>
            ))}
          </div>
        </div>
      </div>
    )
  }
}

export { Quality }
export default inject('store')(Quality)
import React, { Component } from 'react'
import { observable, action } from 'mobx'
import { observer, inject } from 'mobx-react'
import classNames from 'classnames/bind'
import Icon from 'fa/icon'
import styles from './index.scss'
const cx = classNames.bind(styles)

@observer
class Quality extends Component {
  @observable visible = false
  
  @action onToggle() {
    this.visible = !this.visible
  }
  
  @action setStream(stream) {
    this.props.store.setCurrentStream(stream)
    this.visible = false
  }
  
  render() {
    const { store } = this.props
    const className = cx({
      'quality-picker__dropdown': true,
      'quality-picker__dropdown--visible': this.visible
    })
    
    if(!store.isReady) {
      return (
        <div className={cx('button')}><Icon name="cog"/></div>
      )
    }
    
    return (
      <div>
        <div className={cx('button', 'quality-picker')} onClick={this.onToggle.bind(this)}>
          {store.currentStream && store.currentStream.shortLabel}
        </div>
        <div>
          <div className={className}>
            {store.video.getVideoStreams().map((stream) => (
              <div className={cx('quality-picker__item')}
                   onClick={this.setStream.bind(this, stream)}
                   key={stream.format}>
                {stream.label}
              </div>
            ))}
          </div>
        </div>
      </div>
    )
  }
}

export { Quality }
export default inject('store')(Quality)

local state

calling store methods

different rendering paths based on store state

behaviour (user action), localized

behaviour (user action), from store

more local state

local state testing

When testing component that has local state, we should not check the implementation but rather how it behaves (behaviour test)

it('shows dropdown on click', () => {
  const store = {
    isReady: true,
    video: { getVideoStreams: ()=>[] }
  }
  const component = shallow(<Quality store={store}/>)
  component.find('div.button').prop('onClick')()
  component.update()

  expect(component.find('.quality-picker__dropdown--visible').length).toBe(1)
})

This test will not work with react-test-renderer 16.0.0 due to https://github.com/facebook/react/issues/11236

Notice how the store's mock gets more complicated.
You may feel inclined to just pass the actual store here. Don't.

Unfortunately enzyme 3.x is a bit buggy and this is an example. This line should not be necessary but it is :(

calling store methods

There's a contract between store and component.
It needs to be tested against.

it('sets current stream in store', () => {
  const mp4 = { format: 'mp4', label: 'mp4' }
  const store = {
    isReady: true,
    setCurrentStream: sinon.spy(),
    video: { getVideoStreams: () => [mp4] }
  }
  const component = shallow(<Quality store={store}/>)
  component.find('div.button').prop('onClick')()
  component.update()
  component.find('.quality-picker__item').prop('onClick')()
  expect(store.setCurrentStream.calledOnce).toBe(true)
  expect(store.setCurrentStream.calledWith(mp4)).toBe(true)
})

Same object

(same identity)

more local state testing

The clicking behaviour has one more side effect.

You didn't forget about it, did you?

it('hides dropdown after setting stream', () => {
  const mp4 = { format: 'mp4', label: 'mp4' }
  const store = {
    isReady: true,
    setCurrentStream: ()=>{},
    video: { getVideoStreams: () => [mp4] }
  }
  const component = shallow(<Quality store={store}/>)
  component.find('div.button').prop('onClick')()
  component.update()
  expect(component.instance().visible).toBe(true)

  component.find('.quality-picker__item').prop('onClick')()
  component.update()
  expect(component.instance().visible).toBe(false)
})

more than one rendering path

If there's more than one way your component may end up rendering, you should test all alternatives.

it('shows loading spinner when loading', () => {
  const store = {
    isReady: false
  }
  const component = shallow(<Quality store={store}/>)
  expect(component.find('Icon[name="cog"]').length).toBe(1)
})

It would make so much sense to have dedicated <Loading/> component here, wouldn't it?

more than one rendering path

ALL alternatives.
This could've also been a snapshot test.

it('lists streams from store', async () => {
  const mp4 = { format: 'mp4', label: 'mp4' }
  const ogg = { format: 'ogg', label: 'ogg' }
  const store = {
    isReady: true,
    video: { getVideoStreams: () => [ mp4, ogg ] }
  }
  const component = shallow(<Quality store={store}/>)
  expect(component.find('.quality-picker__item').length).toBe(2)
})

any

questions?

SOURCES / RESOURCES

  • http://sinonjs.org/releases/v4.0.1/
  • https://facebook.github.io/jest/docs/en/using-matchers.html
  • https://github.com/airbnb/enzyme/tree/master/docs/api
  • https://github.com/mobxjs/mobx-react#testing-store-injection
  • https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md
  • https://github.com/airbnb/enzyme/pull/368
  • https://segment.com/blog/ui-testing-with-nightmare/
  • https://segment.com/blog/perceptual-diffing-with-niffy/
  • https://codeburst.io/automatic-visual-regression-testing-23cc06471dd
  • https://facebook.github.io/jest/docs/en/snapshot-testing.html
  • https://github.com/adriantoine/enzyme-to-json
  • https://github.com/ctimmerm/axios-mock-adapter
  • https://github.com/wheresrhys/fetch-mock
  • https://percy.io/
  • https://www.npmjs.com/package/delay

Testing React Components

By Michał Matyas

Testing React Components

  • 850