$30 off During Our Annual Pro Sale. View Details »

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.

React Amsterdam

April 21, 2016
Tweet

More Decks by React Amsterdam

Other Decks in Technology

Transcript

  1. Testing React

    View Slide

  2. View Slide

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

    View Slide

  4. View Slide

  5. Testing

    View Slide

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

    View Slide

  7. TDD?

    View Slide

  8. Testing React

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  13. Enter Faucet
    npm install --save-dev faucet

    View Slide

  14. An app to test on

    View Slide

  15. View Slide

  16. View Slide

  17. View Slide

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

    View Slide

  19. But before we dive into
    React testing...

    View Slide

  20. Embrace Plain Old
    JavaScript Objects!

    View Slide

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

    View Slide

  22. Plain old JavaScript
    functions are so easy to test

    View Slide

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

    View Slide

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

    View Slide

  25. Testing React Components

    View Slide

  26. View Slide

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

    View Slide

  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 (

    this.toggleDone() }>{ todo.name }
    this.deleteTodo(e) }>Delete

    )
    }
    }

    View Slide

  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.

    View Slide

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

    View Slide

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

    View Slide

  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(

    );
    return renderer.getRenderOutput();
    }

    View Slide

  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)

    View Slide

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

    View Slide

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

    View Slide

  36. Testing Interactions

    View Slide

  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.

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  41. Testing toggling a Todo

    View Slide

  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(

    );
    // simulate the click here
    });

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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(

    );
    const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p');
    TestUtils.Simulate.click(todoText);
    });

    View Slide

  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(

    );
    const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p');
    TestUtils.Simulate.click(todoText);
    });
    Some people don't like this callback approach to
    testing.

    View Slide

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

    View Slide

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

    View Slide

  50. View Slide

  51. Using Doubles

    View Slide

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

    View Slide

  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] ];

    View Slide

  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(

    );
    const todoText = TestUtils.findRenderedDOMComponentWithTag(result, 'p');
    TestUtils.Simulate.click(todoText);
    t.equal(doneCallback.callCount, 1);
    t.equal(doneCallback.args[0][0], 1);
    }):

    View Slide

  55. Testing form submissions
    Adding a Todo

    View Slide

  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 (


    this.addTodo(e) }>Add Todo

    )
    }
    }

    View Slide

  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(

    );
    ...
    });
    });

    View Slide

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

    View Slide

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

    View Slide

  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(

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

    View Slide

  61. Testing the Todos
    component

    View Slide

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

    View Slide

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

    todo={todo}
    doneChange={(id) => 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)} />

    )
    }
    }

    View Slide

  64. Adding a Todo

    View Slide

  65. 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?
    });

    View Slide

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

    View Slide

  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

    View Slide

  68. 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);
    });

    View Slide

  69. TestUtils is quite verbose

    View Slide

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

    View Slide

  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.

    View Slide

  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.

    View Slide

  73. npm install --save-dev enzyme

    View Slide

  74. Better shallow rendering

    View Slide

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

    View Slide

  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

    View Slide

  77. Better User Interactions

    View Slide

  78. import { mount } from 'enzyme';

    View Slide

  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();
    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

    View Slide

  80. Bonus: testing in browser

    View Slide

  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.

    View Slide

  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'

    View Slide

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

    View Slide

  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.

    View Slide

  85. View Slide

  86. Wrapping up

    View Slide

  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

    View Slide

  88. Thanks!

    View Slide

  89. View Slide