Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing React Applications: React Amsterdam 2016

Testing React Applications: React Amsterdam 2016

A talk on testing React applications.

Jack Franklin

April 16, 2016
Tweet

More Decks by Jack Franklin

Other Decks in Technology

Transcript

  1. Testing React
    @Jack_Franklin
    React Amsterdam 2016
    1

    View Slide

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

    View Slide

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

    View Slide

  4. Testing
    4

    View Slide

  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

    View Slide

  6. TDD or not
    TDD?
    6

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  16. 16

    View Slide

  17. Or...
    17

    View Slide

  18. 18

    View Slide

  19. 19

    View Slide

  20. An app to test on
    20

    View Slide

  21. 21

    View Slide

  22. 22

    View Slide

  23. 23

    View Slide

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

    View Slide

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

    View Slide

  26. Embrace Plain Old
    JavaScript
    Objects!
    26

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  31. Testing React
    Components
    31

    View Slide

  32. 32

    View Slide

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

    View Slide

  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 (

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

    )
    }
    }
    34

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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(

    );
    return renderer.getRenderOutput();
    }
    38

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  42. Testing
    Interactions
    42

    View Slide

  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

    View Slide

  44. DOM
    44

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. Testing toggling a
    Todo
    48

    View Slide

  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

    View Slide

  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(

    );
    // simulate the click here
    });
    50

    View Slide

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

    View Slide

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

    View Slide

  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(

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

    View Slide

  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(

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

    View Slide

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

    View Slide

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

    View Slide

  57. Using Doubles
    57

    View Slide

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

    View Slide

  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

    View Slide

  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(

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

    View Slide

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

    View Slide

  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(

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

    View Slide

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

    View Slide

  64. Testing form
    submissions by
    adding a Todo
    64

    View Slide

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

    View Slide

  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 (


    this.addTodo(e) }>Add Todo

    )
    }
    }
    66

    View Slide

  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(

    );
    ...
    });
    });
    67

    View Slide

  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

    View Slide

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

    View Slide

  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(

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

    View Slide

  71. Testing the Todos
    component
    71

    View Slide

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

    View Slide

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

    )
    }
    }
    73

    View Slide

  74. Adding a Todo
    74

    View Slide

  75. 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

    View Slide

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

    View Slide

  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

    View Slide

  78. 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

    View Slide

  79. TestUtils is quite
    verbose
    79

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  83. npm install --save-dev enzyme
    83

    View Slide

  84. Better shallow
    rendering
    84

    View Slide

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

    View Slide

  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

    View Slide

  87. Better User
    Interactions
    87

    View Slide

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

    View Slide

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

    View Slide

  90. Bonus: testing in
    browser
    90

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  95. 95

    View Slide

  96. 96

    View Slide

  97. Wrapping up
    97

    View Slide

  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

    View Slide

  99. Thanks !
    Any questions?
    99

    View Slide

  100. 100

    View Slide