Testing React Applications: React Amsterdam 2016

Testing React Applications: React Amsterdam 2016

A talk on testing React applications.

Aea964cf59c0c81fff752896f070cbbb?s=128

Jack Franklin

April 16, 2016
Tweet

Transcript

  1. Testing React @Jack_Franklin React Amsterdam 2016 1

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

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

  4. Testing 4

  5. 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
  6. TDD or not TDD? 6

  7. 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
  8. 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
  9. 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
  10. • 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
  11. Running the tests We'll use babel-register to hook Babel into

    our tests. npm install --save-dev babel-register 11
  12. Edit package.json { ..., "scripts": { "test": "tape -r babel-register

    test/*-test.js" }, ... } 12
  13. > 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
  14. But this is pretty ugly! The TAP format isn't designed

    for human eyes 14
  15. 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
  16. 16

  17. Or... 17

  18. 18

  19. 19

  20. An app to test on 20

  21. 21

  22. 22

  23. 23

  24. State is held in <Todos />, and other components notify

    <Todos /> when data has changed. 24
  25. But before we dive into React testing... 25

  26. Embrace Plain Old JavaScript Objects! 26

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

    components 27
  28. Plain old JavaScript functions are so easy to test 28

  29. 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
  30. 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
  31. Testing React Components 31

  32. 32

  33. class Todo extends React.Component { ... } Todo.propTypes = {

    todo: React.PropTypes.object.isRequired, doneChange: React.PropTypes.func.isRequired, deleteTodo: React.PropTypes.func.isRequired } 33
  34. 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 ( <div className={`todo ${className} todo-${todo.id}`}> <p className="toggle-todo" onClick={() => this.toggleDone() }>{ todo.name }</p> <a className="delete-todo" href="#" onClick={(e) => this.deleteTodo(e) }>Delete</a> </div> ) } } 34
  35. • 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
  36. React Test Utils https://facebook.github.io/react/docs/test- utils.html npm install --save-dev react-addons-test-utils 36

  37. 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
  38. 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( <Todo todo={todo} doneChange={fn} deleteTodo={fn}/> ); return renderer.getRenderOutput(); } 38
  39. { "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
  40. 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
  41. 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
  42. Testing Interactions 42

  43. 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
  44. DOM 44

  45. To test interactions we need a DOM. Enter jsdom! npm

    install --save-dev jsdom 45
  46. import jsdom from 'jsdom'; function setupDom() { if (typeof document

    === 'undefined') { global.document = jsdom.jsdom('<html><body></body></html>'); global.window = document.defaultView; global.navigator = window.navigator; } } setupDom(); 46
  47. This has to be imported before React. import './setup'; import

    React from 'react'; // and so on... 47
  48. Testing toggling a Todo 48

  49. 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
  50. 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( <Todo todo={todo} doneChange={doneCallback} /> ); // simulate the click here }); 50
  51. Firstly, find the component on the page that we want

    to click on. const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); 51
  52. Secondly, simulate a click on it. TestUtils.Simulate.click(todoText); 52

  53. 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( <Todo todo={todo} doneChange={doneCallback} /> ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); }); 53
  54. 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( <Todo todo={todo} doneChange={doneCallback} /> ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); }); Some people don't like this callback approach to testing. 54
  55. The ideal test: test('descriptive name here', (t) => { //

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

    // assert on the results // setup // interaction / invoke the thing }); 56
  57. Using Doubles 57

  58. github.com/jackfrankin/doubler npm install --save-dev doubler You might prefer Sinon, TestDouble.js,

    the library doesn't matter :) 58
  59. 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
  60. 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( <Todo todo={todo} doneChange={doneCallback} /> ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); // assertions }): 60
  61. t.equal(doneCallback.callCount, 1); t.equal(doneCallback.args[0][0], 1); 61

  62. 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( <Todo todo={todo} doneChange={doneCallback} /> ); const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p'); TestUtils.Simulate.click(todoText); t.equal(doneCallback.callCount, 1); t.equal(doneCallback.args[0][0], 1); }): 62
  63. The ideal test: test('descriptive name here', (t) => { //

    setup // interaction / invoke the thing // assert on the results }); 63
  64. Testing form submissions by adding a Todo 64

  65. class AddTodo extends Component { ... } AddTodo.propTypes = {

    onNewTodo: React.PropTypes.func.isRequired } 65
  66. 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 ( <div className="add-todo"> <input type="text" placeholder="Walk the dog" ref="todoTitle" /> <button onClick={(e) => this.addTodo(e) }>Add Todo</button> </div> ) } } 66
  67. 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( <AddTodo onNewTodo={todoCallback} /> ); ... }); }); 67
  68. 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
  69. And finally we can make our assertions: t.equal(todoCallback.callCount, 1); t.deepEqual(todoCallback.args[0][0],

    { name: 'Buy Milk' }); 69
  70. 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( <AddTodo onNewTodo={todoCallback} /> ); 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
  71. Testing the Todos component 71

  72. This component encapsulates most of our application, so the tests

    we'll write will be more akin to integration tests. 72
  73. export default class Todos extends Component { constructor(props) {...} toggleDone(id)

    {...} addTodo(todo) {...} deleteTodo(id) {...} renderTodos() { return this.state.todos.map((todo) => { return ( <li key={todo.id}> <Todo todo={todo} doneChange={(id) => this.toggleDone(id)} deleteTodo={(id) => this.deleteTodo(id)} /> </li> ); }); } render() { return ( <div> <p>The <em>best</em> todo app out there.</p> <h1>Things to get done:</h1> <ul className="todos-list">{ this.renderTodos() }</ul> <AddTodo onNewTodo={(todo) => this.addTodo(todo)} /> </div> ) } } 73
  74. Adding a Todo 74

  75. t.test('Adding a todo', (t) => { t.plan(1); const result =

    TestUtils.renderIntoDocument(<Todos />); 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
  76. const todos = TestUtils.scryRenderedDOMComponentsWithClass(result, 'todo'); t.equal(todos.length, 4); 76

  77. 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
  78. t.test('Adding a todo', (t) => { t.plan(1); const result =

    TestUtils.renderIntoDocument(<Todos />); 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
  79. TestUtils is quite verbose 79

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

  81. 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
  82. 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
  83. npm install --save-dev enzyme 83

  84. Better shallow rendering 84

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

    shallow(<Todo todo={todo} ... />); } 85
  86. 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
  87. Better User Interactions 87

  88. import { mount } from 'enzyme'; 88

  89. 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(<AddTodo onNewTodo={todoCallback} />); 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
  90. Bonus: testing in browser 90

  91. 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
  92. 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
  93. Use tape-run to run the tests in an actual browser.

    (Other libraries have similar tools available for this). 93
  94. 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
  95. 95

  96. 96

  97. Wrapping up 97

  98. 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
  99. Thanks ! Any questions? 99

  100. 100