Slide 1

Slide 1 text

Testing React

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Jack Franklin — Pusher.com — javascriptplayground.com — @Jack_Franklin, github.com/jackfranklin

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Testing

Slide 6

Slide 6 text

— confidence — permanently fix bugs — provide support for refactoring — enables teams to move quicker

Slide 7

Slide 7 text

TDD?

Slide 8

Slide 8 text

Testing React

Slide 9

Slide 9 text

Testing Libraries I've taken to using Tape [https://github.com/substack/ tape], but you can use whichever framework you'd like. This talk is test framework agnostic :)

Slide 10

Slide 10 text

import test from 'tape'; test('Adding numbers', (t) => { t.test('2 + 2 is 4', (t) => { t.plan(1); t.equal(2 + 2, 4); }); t.test('some objects are equal', (t) => { t.plan(1); const obj = { a: 1 }; t.deepEqual(obj, { a: 1 }); }); });

Slide 11

Slide 11 text

Running the tests npm install --save-dev babel-tape-runner A runner for Tape tests that will first run them through Babel.

Slide 12

Slide 12 text

> babel-tape-runner test/*-test.js TAP version 13 # Adding numbers # 2 + 2 is 4 ok 1 should be equal # some objects are equal ok 2 should be equivalent 1..2 # tests 2 # pass 2 # ok

Slide 13

Slide 13 text

Enter Faucet npm install --save-dev faucet

Slide 14

Slide 14 text

An app to test on

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

State is held in , and other components notify when data has changed.

Slide 19

Slide 19 text

But before we dive into React testing...

Slide 20

Slide 20 text

Embrace Plain Old JavaScript Objects!

Slide 21

Slide 21 text

Most of your logic should exist outside of your React components

Slide 22

Slide 22 text

Plain old JavaScript functions are so easy to test

Slide 23

Slide 23 text

state-functions.js export function toggleDone(state, id) { ... } export function addTodo(state, todo) { ... } export function deleteTodo(state, id) { ... }

Slide 24

Slide 24 text

Testing these are easy and require no set up at all! test('addTodo', (t) => { t.test('it can add a new todo and set the right id', (t) => { t.plan(1); const initialState = { todos: [{ id: 1, name: 'Buy Milk', done: true }] }; const newState = addTodo(initialState, { name: 'Get bread' }); t.deepEqual(newState.todos[1], { name: 'Get bread', id: 2, done: false }); }); });

Slide 25

Slide 25 text

Testing React Components

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

class Todo extends React.Component { ... } Todo.propTypes = { todo: React.PropTypes.object.isRequired, doneChange: React.PropTypes.func.isRequired, deleteTodo: React.PropTypes.func.isRequired }

Slide 28

Slide 28 text

import React, { Component } from 'react'; export default class Todo extends Component { toggleDone() { this.props.doneChange(this.props.todo.id); } deleteTodo(e) { e.preventDefault(); this.props.deleteTodo(this.props.todo.id); } render() { const { todo } = this.props; const className = todo.done ? 'done-todo' : ''; return (

this.toggleDone() }>{ todo.name }

this.deleteTodo(e) }>Delete
) } }

Slide 29

Slide 29 text

— The rendered div is given a class of done-todo if the todo is done. — Deleting a todo calls this.props.deleteTodo with the right ID. — Clicking on a todo calls this.props.toggleDone with the right ID.

Slide 30

Slide 30 text

React Test Utils https://facebook.github.io/react/docs/test-utils.html npm install --save-dev react-addons-test-utils

Slide 31

Slide 31 text

Shallow Rendering Test without a DOM; we don't actually render components but get a representation of what the rendered component will look like. Keeps tests nice and quick :)

Slide 32

Slide 32 text

todo-test.js import React from 'react'; import Todo from '../app/todo'; import TestUtils from 'react-addons-test-utils'; import test from 'tape'; function shallowRenderTodo(todo) { const renderer = TestUtils.createRenderer(); const fn = () => {}; renderer.render( ); return renderer.getRenderOutput(); }

Slide 33

Slide 33 text

{ "type": "div", "props": { "className": "todo todo-1", "children": [ { "type": "p", "props": { "className": "toggle-todo", "children": "Buy Milk" }, }, { "type": "a", "props": { "className": "delete-todo", "href": "#", "children": "Delete" }, } ] }, } (Some items removed to save space)

Slide 34

Slide 34 text

test('Todo component', (t) => { t.test('rendering a not-done tweet', (t) => { const todo = { id: 1, name: 'Buy Milk', done: false }; const result = shallowRenderTodo(todo); t.test('It renders the text of the todo', (t) => { t.plan(1); t.equal(result.props.children[0].props.children, 'Buy Milk'); }); }); });

Slide 35

Slide 35 text

test('Todo component', (t) => { // other tests omitted t.test('rendering a done tweet', (t) => { const todo = { id: 1, name: 'Buy Milk', done: true }; const result = shallowRenderTodo(todo); t.test('The todo does have the done class', (t) => { t.plan(1); t.ok(result.props.className.indexOf('done-todo') > -1); }); }); });

Slide 36

Slide 36 text

Testing Interactions

Slide 37

Slide 37 text

React components will often have some state (like our todos) that users can edit. You should never directly call React component methods in tests. Your input should be user interaction, and the output that's tested should be the rendered component.

Slide 38

Slide 38 text

DOM To test interactions we need a DOM. Enter jsdom! npm install --save-dev jsdom

Slide 39

Slide 39 text

import jsdom from 'jsdom'; function setupDom() { if (typeof document === 'undefined') { global.document = jsdom.jsdom(''); global.window = document.defaultView; global.navigator = window.navigator; } } setupDom();

Slide 40

Slide 40 text

This has to be imported before React. import './setup'; import React from 'react'; // and so on...

Slide 41

Slide 41 text

Testing toggling a Todo

Slide 42

Slide 42 text

t.test('toggling a TODO calls the given prop', (t) => { t.plan(1); // our assertion is made in the doneCallback fn // so when the click is made, it will be run const doneCallback = (id) => t.equal(id, 1); const todo = { id: 1, name: 'Buy Milk', done: false }; const result = TestUtils.renderIntoDocument( ); // simulate the click here });

Slide 43

Slide 43 text

TestUtils.renderIntoDocument(...) Render a component into a detached DOM node in the document. [https://facebook.github.io/react/docs/test-utils.html]

Slide 44

Slide 44 text

Firstly, find the component on the page that we want to click on. const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p');

Slide 45

Slide 45 text

Secondly, simulate a click on it. TestUtils.Simulate.click(todoText);

Slide 46

Slide 46 text

t.test('toggling a TODO calls the given prop', (t) => { t.plan(1); const doneCallback = (id) => t.equal(id, 1); const todo = { id: 1, name: 'Buy Milk', done: false }; const result = TestUtils.renderIntoDocument( ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); });

Slide 47

Slide 47 text

t.test('toggling a TODO calls the given prop', (t) => { t.plan(1); const doneCallback = (id) => t.equal(id, 1); const todo = { id: 1, name: 'Buy Milk', done: false }; const result = TestUtils.renderIntoDocument( ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); }); Some people don't like this callback approach to testing.

Slide 48

Slide 48 text

The ideal test: test('descriptive name here', (t) => { // setup // interaction / invoke the thing // assert on the results });

Slide 49

Slide 49 text

We've got it backwards! test('descriptive name here', (t) => { // assert on the results // setup // interaction / invoke the thing });

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

Using Doubles

Slide 52

Slide 52 text

github.com/jackfrankin/doubler npm install --save-dev doubler You might prefer Sinon, TestDouble.js, the library doesn't matter :)

Slide 53

Slide 53 text

A double is just a function that keeps track of its calls: import { Double } from 'doubler'; const x = Double.function(); x(2); x(3); x.callCount === 2; x.args === [ [2], [3] ];

Slide 54

Slide 54 text

import { Double } from 'doubler'; t.test('toggling a TODO calls the given prop', (t) => { t.plan(2); const doneCallback = Double.function(); const todo = { id: 1, name: 'Buy Milk', done: false }; const result = mount( ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); t.equal(doneCallback.callCount, 1); t.equal(doneCallback.args[0][0], 1); }):

Slide 55

Slide 55 text

Testing form submissions Adding a Todo

Slide 56

Slide 56 text

import React, { Component } from 'react'; export default class AddTodo extends Component { addTodo(e) { e.preventDefault(); const newTodoName = this.refs.todoTitle.value; if (newTodoName) { this.props.onNewTodo({ name: newTodoName }); this.refs.todoTitle.value = ''; } } render() { return (
this.addTodo(e) }>Add Todo
) } }

Slide 57

Slide 57 text

We first render AddTodo, giving it a double as a callback: test('Add Todo component', (t) => { t.test('it calls the given callback prop with the new text', (t) => { t.plan(2); const todoCallback = Double.function(); const form = TestUtils.renderIntoDocument( ); ... }); });

Slide 58

Slide 58 text

Then we can find and fill out the form input: const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); input.value = 'Buy Milk'; Before then clicking a button: const button = TestUtils.findRenderedDOMComponentWithTag(form, 'button'); TestUtils.Simulate.click(button);

Slide 59

Slide 59 text

And finally we can make our assertions: t.equal(todoCallback.callCount, 1); t.deepEqual(todoCallback.args[0][0], { name: 'Buy Milk' });

Slide 60

Slide 60 text

test('Add Todo component', (t) => { t.test('it calls the given callback prop with the new text', (t) => { t.plan(2); const todoCallback = Double.function(); const form = TestUtils.renderIntoDocument( ); const input = TestUtils.findRenderedDOMComponentWithTag(form, 'input'); input.value = 'Buy Milk'; const button = TestUtils.findRenderedDOMComponentWithTag(form, 'button'); TestUtils.Simulate.click(button); t.equal(todoCallback.callCount, 1); t.deepEqual(todoCallback.args[0][0], { name: 'Buy Milk' }); }); });

Slide 61

Slide 61 text

Testing the Todos component

Slide 62

Slide 62 text

This component encapsulates most of our application, so the tests we'll write will be more akin to integration tests.

Slide 63

Slide 63 text

export default class Todos extends Component { constructor(props) {...} toggleDone(id) {...} addTodo(todo) {...} deleteTodo(id) {...} renderTodos() { return this.state.todos.map((todo) => { return (
  • this.toggleDone(id)} deleteTodo={(id) => this.deleteTodo(id)} />
  • ); }); } render() { return (

    The best todo app out there.

    Things to get done:

      { this.renderTodos() }
    this.addTodo(todo)} />
    ) } }

    Slide 64

    Slide 64 text

    Adding a Todo

    Slide 65

    Slide 65 text

    t.test('Adding a todo', (t) => { t.plan(1); const result = TestUtils.renderIntoDocument(); const formInput = TestUtils.findRenderedDOMComponentWithTag(result, 'input'); formInput.value = 'Buy Milk'; const button = TestUtils.findRenderedDOMComponentWithTag(result, 'button'); TestUtils.Simulate.click(button); // what assertion to make? });

    Slide 66

    Slide 66 text

    const todos = TestUtils.scryRenderedDOMComponentsWithClass(result, 'todo'); t.equal(todos.length, 4);

    Slide 67

    Slide 67 text

    scryRenderedDOMComponentsWithClass finds all components with the given class, and returns an array. Scrying (also called seeing or peeping) is the practice of looking into a translucent ball

    Slide 68

    Slide 68 text

    t.test('Adding a todo', (t) => { t.plan(1); const result = TestUtils.renderIntoDocument(); const formInput = TestUtils.findRenderedDOMComponentWithTag(result, 'input'); formInput.value = 'Buy Milk'; const button = TestUtils.findRenderedDOMComponentWithTag(result, 'button'); TestUtils.Simulate.click(button); const todos = TestUtils.scryRenderedDOMComponentsWithClass(result, 'todo'); t.equal(todos.length, 4); });

    Slide 69

    Slide 69 text

    TestUtils is quite verbose

    Slide 70

    Slide 70 text

    Enter Enzyme! http://airbnb.io/enzyme/

    Slide 71

    Slide 71 text

    Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output. Enzyme's API is meant to be intuitive and flexible by mimicking jQuery's API for DOM manipulation and traversal.

    Slide 72

    Slide 72 text

    Enzyme is unopinionated regarding which test runner or assertion library you use, and should be compatible with all major test runners and assertion libraries out there.

    Slide 73

    Slide 73 text

    npm install --save-dev enzyme

    Slide 74

    Slide 74 text

    Better shallow rendering

    Slide 75

    Slide 75 text

    import { shallow } from 'enzyme'; function shallowRenderTodo(todo) { return shallow(); }

    Slide 76

    Slide 76 text

    t.test('rendering a not-done tweet', (t) => { const todo = { id: 1, name: 'Buy Milk', done: false }; const result = shallowRenderTodo(todo); t.test('It renders the text of the todo', (t) => { t.plan(1); t.equal(result.find('p').text(), 'Buy Milk'); }); }); Shallow rendering API docs: http://airbnb.io/enzyme/ docs/api/shallow.html

    Slide 77

    Slide 77 text

    Better User Interactions

    Slide 78

    Slide 78 text

    import { mount } from 'enzyme';

    Slide 79

    Slide 79 text

    test('Add Todo component', (t) => { t.test('it calls the given callback prop with the new text', (t) => { t.plan(2); const todoCallback = Double.function(); const form = mount(); const input = form.find('input').get(0); input.value = 'Buy Milk'; form.find('button').simulate('click'); t.equal(todoCallback.callCount, 1); t.deepEqual(todoCallback.args[0][0], { name: 'Buy Milk' }); }); }); Full rendering API docs: http://airbnb.io/enzyme/docs/ api/mount.html

    Slide 80

    Slide 80 text

    Bonus: testing in browser

    Slide 81

    Slide 81 text

    In development it's nice to be able to run in the terminal, but before deploying you should run your tests across all the browsers you support.

    Slide 82

    Slide 82 text

    First, build a bundle of all your tests using Browserify / Webpack / your tool of choice. browserify test/*-test.js \ -t [ babelify --presets [ es2015 react ] ] \ -u 'react/lib/ReactContext' \ -u 'react/lib/ExecutionEnvironment'

    Slide 83

    Slide 83 text

    Use tape-run to run the tests in an actual browser.

    Slide 84

    Slide 84 text

    browserify test/*-test.js \ -t [ babelify --presets [ es2015 react ] ] \ -u 'react/lib/ReactContext' \ -u 'react/lib/ExecutionEnvironment' \ | tape-run -b chrome Tests are run in an actual browser instance; results are sent back to the terminal.

    Slide 85

    Slide 85 text

    No content

    Slide 86

    Slide 86 text

    Wrapping up

    Slide 87

    Slide 87 text

    Blog post: http://12devsofxmas.co.uk/2015/12/day-2- testing-react-applications/ Repo with Tests: https://github.com/jackfranklin/todo- react-testing Me: @Jack_Franklin, javascriptplayground.com

    Slide 88

    Slide 88 text

    Thanks!

    Slide 89

    Slide 89 text

    No content