Jack Franklin, Pusher, London — Testing React Applications

Jack Franklin, Pusher, London — Testing React Applications

Jack Franklin, Pusher, London — Testing React Applications

We'll discuss the best tooling and approaches for building ReactJS applications in the NodeJS environment and in the browser using React TestUtils and other third party libraries that make it easy to test your components. We'll see how to build components in a way that makes them more maintainable, testable and easier to work with.

5799a3c0434b91ef7e00e730629390f0?s=128

React Amsterdam

April 21, 2016
Tweet

Transcript

  1. Testing React

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

  4. None
  5. Testing

  6. — confidence — permanently fix bugs — provide support for

    refactoring — enables teams to move quicker
  7. TDD?

  8. Testing React

  9. 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 :)
  10. 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 }); }); });
  11. Running the tests npm install --save-dev babel-tape-runner A runner for

    Tape tests that will first run them through Babel.
  12. > 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
  13. Enter Faucet npm install --save-dev faucet

  14. An app to test on

  15. None
  16. None
  17. None
  18. State is held in <Todos />, and other components notify

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

  20. Embrace Plain Old JavaScript Objects!

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

    components
  22. Plain old JavaScript functions are so easy to test

  23. state-functions.js export function toggleDone(state, id) { ... } export function

    addTodo(state, todo) { ... } export function deleteTodo(state, id) { ... }
  24. 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 }); }); });
  25. Testing React Components

  26. None
  27. class Todo extends React.Component { ... } Todo.propTypes = {

    todo: React.PropTypes.object.isRequired, doneChange: React.PropTypes.func.isRequired, deleteTodo: React.PropTypes.func.isRequired }
  28. 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> ) } }
  29. — 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.
  30. React Test Utils https://facebook.github.io/react/docs/test-utils.html npm install --save-dev react-addons-test-utils

  31. 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 :)
  32. 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(); }
  33. { "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)
  34. 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'); }); }); });
  35. 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); }); }); });
  36. Testing Interactions

  37. 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.
  38. DOM To test interactions we need a DOM. Enter jsdom!

    npm install --save-dev jsdom
  39. 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();
  40. This has to be imported before React. import './setup'; import

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

  42. 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 });
  43. TestUtils.renderIntoDocument(...) Render a component into a detached DOM node in

    the document. [https://facebook.github.io/react/docs/test-utils.html]
  44. Firstly, find the component on the page that we want

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

  46. 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); });
  47. 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.
  48. The ideal test: test('descriptive name here', (t) => { //

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

    // assert on the results // setup // interaction / invoke the thing });
  50. None
  51. Using Doubles

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

    the library doesn't matter :)
  53. 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] ];
  54. 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( <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); }):
  55. Testing form submissions Adding a Todo

  56. 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 ( <div className="add-todo"> <input type="text" placeholder="Walk the dog" ref="todoTitle" /> <button onClick={(e) => this.addTodo(e) }>Add Todo</button> </div> ) } }
  57. 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} /> ); ... }); });
  58. 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);
  59. And finally we can make our assertions: t.equal(todoCallback.callCount, 1); t.deepEqual(todoCallback.args[0][0],

    { name: 'Buy Milk' });
  60. 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' }); }); });
  61. Testing the Todos component

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

    we'll write will be more akin to integration tests.
  63. 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> ) } }
  64. Adding a Todo

  65. 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? });
  66. const todos = TestUtils.scryRenderedDOMComponentsWithClass(result, 'todo'); t.equal(todos.length, 4);

  67. 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
  68. 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); });
  69. TestUtils is quite verbose

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

  71. 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.
  72. 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.
  73. npm install --save-dev enzyme

  74. Better shallow rendering

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

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

  78. import { mount } from 'enzyme';

  79. 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
  80. Bonus: testing in browser

  81. 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.
  82. 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'
  83. Use tape-run to run the tests in an actual browser.

  84. 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.
  85. None
  86. Wrapping up

  87. 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
  88. Thanks!

  89. None