Slide 1

Slide 1 text

Testing React Components LELAND RICHARDSON / AIRBNB / @INTELLIGIBABBLE Video: http://bit.ly/react-testing-pres

Slide 2

Slide 2 text

Testing the UI First, Let’s talk about testing UIs in general

Slide 3

Slide 3 text

Before React, testing UI was hard Before React, testing UI was hard.

Slide 4

Slide 4 text

After React, testing UI was possible …but still kind of hard. Once React came along, testing UI was possible But… for some reason, testing was still kind of hard. :(

Slide 5

Slide 5 text

UI = f(state) With React, our entire UI is essentially just a pure function of our application’s state at an instant in time. And pure functions should be easy to test… so what is making it hard to test?

Slide 6

Slide 6 text

View View View View View View One problem is scope.

Slide 7

Slide 7 text

View View View View View View Isolated Integrated Testing leaf node components is fairly straightforward. The tests are nicely isolated.

Slide 8

Slide 8 text

View View View View View View Isolated Integrated As we move up the tree, we are not only testing the components at that level, but also all of the components in the levels below.

Slide 9

Slide 9 text

github.com The Answer? Shallow Rendering The answer to this problem is the “shallow renderer”, which React has had since 0.13.

Slide 10

Slide 10 text

View View View View View View The shallow renderer returns us the output of just a single component, regardless of how high up the render tree it is.

Slide 11

Slide 11 text

import ToDoList from '../components/ToDoList'; describe('ToDoList', () => { it('renders correctly', () => { // setup var items = [/* ... */] // render shallowRenderer.render(); var output = shallowRenderer.getRenderOutput(); // assert expect(output.type).to.equal('div'); expect(output.props.className).to.equal('row row-space-2'); expect(output.props.children[0].type).to.equal('div'); expect(output.props.children[0].props.className) .to.equal('todo-item col-sm-6'); expect(output.props.children[0].props.children[0].props.children) .to.equal(items[0].title); }); }); the shallow renderer gives us what we want: a slice of the render tree, but we aren’t provided with any tools to inspect it.

Slide 12

Slide 12 text

import ToDoList from '../components/ToDoList'; describe('ToDoList', () => { it('renders correctly', () => { // setup var items = [/* ... */] // render shallowRenderer.render(); var output = shallowRenderer.getRenderOutput(); // assert expect(output.type).to.equal('div'); expect(output.props.className).to.equal('row row-space-2'); expect(output.props.children[0].type).to.equal('div'); expect(output.props.children[0].props.className) .to.equal('todo-item col-sm-6'); expect(output.props.children[0].props.children[0].props.children) .to.equal(items[0].title); }); }); As a result, we end up coupling our tests to our component implementation, rather than just the component behaviors we want to test.

Slide 13

Slide 13 text

Enzyme

Slide 14

Slide 14 text

$ npm i enzyme import { shallow } from 'enzyme'; const wrapper = shallow(); Enzyme exports a shallow function which you pass a react element and get out a jquery-like wrapper around the resulting tree.

Slide 15

Slide 15 text

import { shallow } from 'enzyme'; import ToDoList from '../components/ToDoList'; import ToDoItem from '../components/ToDoItem'; describe('ToDoList', () => { it('renders n items', () => { const items = [/* ... */] const wrapper = shallow(); expect(wrapper.find(ToDoItem)).to.have.length(items.length); }); }); describe('ToDoItem', () => { it('renders the title', () => { const item = {/* ... */}; const wrapper = shallow(); expect(wrapper.text()).contains(item.title); }); }); Enzyme allows us to write the same tests that we had written before, but they are less coupled to our implementation. We can find nodes of a specific type in our tree by just passing in the component constructor

Slide 16

Slide 16 text

import { shallow } from 'enzyme'; import Counter from '../components/Counter'; describe('.simulate(event[,data])', () => { it('calls event handlers', () => { const wrapper = shallow(); expect(wrapper.state().count).to.equal(0); wrapper.find('button').simulate('click'); expect(wrapper.state().count).to.equal(1); }); }); Enzyme also allows you to easily simulate events and assert on state transitions.

Slide 17

Slide 17 text

const wrapper = shallow(
Baz ); console.log(wrapper.debug()); // //
// // Baz // // console.log(wrapper.find('.bar').debug()); // // Baz // Enzyme exposes “debug” methods for you to easily figure out why your test is failing. Tests are useful tools for telling us when something is wrong. But often it is difficult to understand *why* something is wrong. Logging a React render tree can often result in your terminal window exploding.

Slide 18

Slide 18 text

import { shallow, mount, render, } from 'enzyme'; In addition to “shallow”, enzyme exports “mount” and “render” methods that do full DOM rendering and HTML rendering. They each return wrappers with identical APIs to shallow.

Slide 19

Slide 19 text

What about React Native?

Slide 20

Slide 20 text

Testing React Native is hard • React Native runs on node 5 + npm 3. Jest has issues with this. • React Native is dependent on Packager. • NativeModules require Device. Solution? If you just blindly try and test React Native today, it’s pretty hard for several reasons. So what’s the solution? If you look online right now you might find some people telling you to mock it… so let’s see what that entails

Slide 21

Slide 21 text

/* ~/__mocks__/react-native.js */ var React = module.exports = require('react'); const mockComponent = () => React.createClass({ render: () => null, }); React.View = mockComponent(); React.Text = mockComponent(); React.StyleSheet = { create: x => x }; React.Image = mockComponent(); React.Picker = mockComponent(); React.TextInput = mockComponent(); React.TouchableOpacity = mockComponent(); // ... React.Animated = /* OH GAWD. WHAT NOW?! */ Since React Native just uses React itself under the hood, the react module turns out to be a pretty good starting point for a mock. But then of course, you quickly figure out that almost every one of your components has at least a View node, a Text node, and a stylesheet object… so we create some simple “mock” components for those that just return null. But React Native has 38 component APIs that it exports, and 35 non- component APIs. Many of them that you are using can end up being pretty complicated. At this point, you are wondering just how deep you are going to need to go just to get a single little test to pass.

Slide 22

Slide 22 text

Write tests, not mocks Bottom line: you should be spending your time writing tests, not mocks.

Slide 23

Slide 23 text

$ npm i react-native-mock So I decided to make a library that mocked the entire react native API for you, so you don’t have to.

Slide 24

Slide 24 text

/* package.json */ { "name": "my_react_native_app", "scripts": { "test": "mocha --require react-native-mock/mock" // ... }, "devDependencies": { "enzyme": "^2.0.0", "react-native-mock": "^0.0.6", // ... }, // ... } It’s all just plain JavaScript, so you can run your tests with any test runner you’d like.

Slide 25

Slide 25 text

/* package.json */ { "name": "my_react_native_app", "scripts": { "test": "mocha --require react-native-mock/mock" // ... }, "devDependencies": { "enzyme": "^2.0.0", "react-native-mock": "^0.0.6", // ... }, // ... } All you have to do is require the /mock entry file of the library, and subsequent requires of react-native will be the mocked API. You will want to require this before anything in your test suite runs. With libraries like mocha, you can even do this from a command-line argument.

Slide 26

Slide 26 text

import React, { Image, TextInput } from 'react-native'; import { shallow } from 'enzyme'; import Login from '../src/Login'; describe('', () => { it('renders the logo', () => { const wrapper = shallow(); expect(wrapper.find(Image)).to.have.length(1); }); it('renders a username and password', () => { const wrapper = shallow(); expect(wrapper.find(TextInput)).to.have.length(2); }); }); And with the mock loaded, you can shallow render and test your React Native components with enzyme just like you would with React.

Slide 27

Slide 27 text

Where do we go from here? So where do we go from here?

Slide 28

Slide 28 text

Integration Testing Integration tests compliment Unit Tests really well. Enzyme could be retrofitted with popular integration test frameworks to make integration tests easier to write, and more “react aware”.

Slide 29

Slide 29 text

Improve Event Simulation Basic event simulation is supported in enzyme, but complicated interaction such as drag and drop or typing is not easy to realistically simulate. Some work could be done here, and it wouldn’t have to be specific to enzyme or react at all.

Slide 30

Slide 30 text

Improve React Native Mock For React Native, this mock could get more and more sophisticated and better emulate a real device.. Ideally though, we can get RN to a point where this is as slim as possible…

Slide 31

Slide 31 text

Thank You github.com/airbnb/enzyme twitter.com/intelligibabble github.com/lelandrichardson github.com/lelandrichardson/react-native-mock