Slide 1

Slide 1 text

Testing React @Jack_Franklin React Amsterdam 2016 1

Slide 2

Slide 2 text

• pusher.com • @pusher • come and get stickers! 2

Slide 3

Slide 3 text

@Jack_Franklin • javascriptplayground.com • github.com/jackfranklin 3

Slide 4

Slide 4 text

Testing 4

Slide 5

Slide 5 text

Testing our code gives us: • confidence • permanently fix bugs by fixing them with a test • provide support for refactoring • enables teams to move quicker 5

Slide 6

Slide 6 text

TDD or not TDD? 6

Slide 7

Slide 7 text

JavaScript Testing Libraries I prefer Tape [https://github.com/substack/tape], but there's also: • Mocha • Jasmine • Jest • ... This talk is test framework agnostic :) 7

Slide 8

Slide 8 text

ES2015 + Babel Arrow functions: (t) => {...} function(t) {...} Imports: import foo from 'bar' ; var foo = require('bar'); import { foo } from 'bar'; var foo = require('bar').foo; 8

Slide 9

Slide 9 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 }); }); }); 9

Slide 10

Slide 10 text

• Tell tape how many tests you're expecting with t.plan(x) • Using t.plan means Tape has good async support out the box • Use t.equal, t.ok, t.deepEqual (and others) • Has good support for Babel • Fairly minimal :) 10

Slide 11

Slide 11 text

Running the tests We'll use babel-register to hook Babel into our tests. npm install --save-dev babel-register 11

Slide 12

Slide 12 text

Edit package.json { ..., "scripts": { "test": "tape -r babel-register test/*-test.js" }, ... } 12

Slide 13

Slide 13 text

> tape -r babel-register 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 13

Slide 14

Slide 14 text

But this is pretty ugly! The TAP format isn't designed for human eyes 14

Slide 15

Slide 15 text

Enter Faucet npm install --save-dev faucet "scripts": { "test": "tape -r babel-register test/*-test.js | faucet" }, There are many, many options here for this! 15

Slide 16

Slide 16 text

16

Slide 17

Slide 17 text

Or... 17

Slide 18

Slide 18 text

18

Slide 19

Slide 19 text

19

Slide 20

Slide 20 text

An app to test on 20

Slide 21

Slide 21 text

21

Slide 22

Slide 22 text

22

Slide 23

Slide 23 text

23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

But before we dive into React testing... 25

Slide 26

Slide 26 text

Embrace Plain Old JavaScript Objects! 26

Slide 27

Slide 27 text

Most of your logic should exist outside of your React components 27

Slide 28

Slide 28 text

Plain old JavaScript functions are so easy to test 28

Slide 29

Slide 29 text

state-functions.js export function toggleDone(state, id) { ... } export function addTodo(state, todo) { ... } export function deleteTodo(state, id) { ... } (See also: Redux and related projects) 29

Slide 30

Slide 30 text

Testing these are easy 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 }); }); }); 30

Slide 31

Slide 31 text

Testing React Components 31

Slide 32

Slide 32 text

32

Slide 33

Slide 33 text

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

Slide 34

Slide 34 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
) } } 34

Slide 35

Slide 35 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. 35

Slide 36

Slide 36 text

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

Slide 37

Slide 37 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 :) 37

Slide 38

Slide 38 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(); } 38

Slide 39

Slide 39 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) 39

Slide 40

Slide 40 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'); }); }); }); 40

Slide 41

Slide 41 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); }); }); }); 41

Slide 42

Slide 42 text

Testing Interactions 42

Slide 43

Slide 43 text

React components will often have some state (like our todos) that users can edit. Input: User Actions (clicks, form fills, etc) Output: Rendered React components Never reach into a component to get or set state. 43

Slide 44

Slide 44 text

DOM 44

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Testing toggling a Todo 48

Slide 49

Slide 49 text

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

Slide 50

Slide 50 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 }); 50

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 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); }); 53

Slide 54

Slide 54 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. 54

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Using Doubles 57

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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 = TestUtils.renderIntoDocument( ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); // assertions }): 60

Slide 61

Slide 61 text

t.equal(doneCallback.callCount, 1); t.equal(doneCallback.args[0][0], 1); 61

Slide 62

Slide 62 text

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 = TestUtils.renderIntoDocument( ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); t.equal(doneCallback.callCount, 1); t.equal(doneCallback.args[0][0], 1); }): 62

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Testing form submissions by adding a Todo 64

Slide 65

Slide 65 text

class AddTodo extends Component { ... } AddTodo.propTypes = { onNewTodo: React.PropTypes.func.isRequired } 65

Slide 66

Slide 66 text

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
) } } 66

Slide 67

Slide 67 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( ); ... }); }); 67

Slide 68

Slide 68 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); 68

Slide 69

Slide 69 text

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

Slide 70

Slide 70 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' }); }); }); 70

Slide 71

Slide 71 text

Testing the Todos component 71

Slide 72

Slide 72 text

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

Slide 73

Slide 73 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)} />
    ) } } 73

    Slide 74

    Slide 74 text

    Adding a Todo 74

    Slide 75

    Slide 75 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? }); 75

    Slide 76

    Slide 76 text

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

    Slide 77

    Slide 77 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 77

    Slide 78

    Slide 78 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); }); 78

    Slide 79

    Slide 79 text

    TestUtils is quite verbose 79

    Slide 80

    Slide 80 text

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

    Slide 81

    Slide 81 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. 81

    Slide 82

    Slide 82 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. 82

    Slide 83

    Slide 83 text

    npm install --save-dev enzyme 83

    Slide 84

    Slide 84 text

    Better shallow rendering 84

    Slide 85

    Slide 85 text

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

    Slide 86

    Slide 86 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 86

    Slide 87

    Slide 87 text

    Better User Interactions 87

    Slide 88

    Slide 88 text

    import { mount } from 'enzyme'; 88

    Slide 89

    Slide 89 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 89

    Slide 90

    Slide 90 text

    Bonus: testing in browser 90

    Slide 91

    Slide 91 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. 91

    Slide 92

    Slide 92 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' 92

    Slide 93

    Slide 93 text

    Use tape-run to run the tests in an actual browser. (Other libraries have similar tools available for this). 93

    Slide 94

    Slide 94 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. 94

    Slide 95

    Slide 95 text

    95

    Slide 96

    Slide 96 text

    96

    Slide 97

    Slide 97 text

    Wrapping up 97

    Slide 98

    Slide 98 text

    Blog post: http://12devsofxmas.co.uk/2015/12/ day-2-testing-react-applications/ Repo with Tests: https://github.com/jackfranklin/ todo-react-testing Slides (in a bit): speakerdeck.com/jackfranklin Me: @Jack_Franklin, javascriptplayground.com 98

    Slide 99

    Slide 99 text

    Thanks ! Any questions? 99

    Slide 100

    Slide 100 text

    100