Tests

Quentin Roy

Quentin Roy

Tests

  • Part 1: (Quick) Introduction

    • Where you speak
  • Part 2: Unit Testing with Jest

    • Where you code

Part 1: (Quick) Introduction

Why would you want to test?

  • Reliability
  • Maintainability
  • Compliance to specifications

Types of Tests

What is a good unit test?

  • Isolated
    1. Only depends on what is tested
    2. Have no impact on the other tests

Can you test anything?

  • Code can be hard to test
  • Code can be impossible to test
  • Solution?
  • Design testable code!

What makes code hard to test?

  • Global, static, singletons
  • Deep inheritance
  • Locked dependencies
    • sub-objects creation (beware of new)
    • Use dependency injection

Test Driven Development

#TDD

Test Driven Development

  1. Write the test
    • It will fail (since the code is not here yet)
  2. Make it pass (write the code)
    • With the least amount of work
  3. In this order

#TDD

Part 2: Unit Testing with Jest

  1. Anatomy of a test
  2. UI testing
  3. Mocking

To your keyboards!

git clone https://github.com/QuentinRoy/jest-tutorial.git
cd jest-tutorial
npm install
npm test
You already run this, didn't you? ;-)

To your keyboards!

npm start

Organization

Anatomy Of a Test

First, an example

src/actions/index.js
let nextTodoId = 0
export const addTodo = (text) => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})
 
export const toggleTodo = (id) => ({
  type: 'TOGGLE_TODO',
  id
})
Static var:-/

Where is the test?

src/actions/index.spec.js
  • Test files can be (by default)
    • */__tests__/*.js
    • *.test.js
    • *.spec.js
We will use that today
src/actions/index.spec.js
import * as actions from './index'

describe('todo actions', () => {
  it('addTodo should create ADD_TODO action', () => {
    expect(actions.addTodo('Use Redux')).toEqual({
      type: 'ADD_TODO',
      id: 0,
      text: 'Use Redux'
    })
  })

  it('toggleTodo should create TOGGLE_TODO action', () => {
    expect(actions.toggleTodo(1)).toEqual({
      type: 'TOGGLE_TODO',
      id: 1
    })
  })
})

describe defines a test suite


import * as actions from './index'

describe('todo actions', () => {
  it('addTodo should create ADD_TODO action', () => {
    expect(actions.addTodo('Use Redux')).toEqual({
      type: 'ADD_TODO',
      id: 0,
      text: 'Use Redux'
    })
  })

  it('toggleTodo should create TOGGLE_TODO action', () => {
    expect(actions.toggleTodo(1)).toEqual({
      type: 'TOGGLE_TODO',
      id: 1
    })
  })
})

it defines a test


import * as actions from './index'

describe('todo actions', () => {
  it('addTodo should create ADD_TODO action', () => {
    expect(actions.addTodo('Use Redux')).toEqual({
      type: 'ADD_TODO',
      id: 0,
      text: 'Use Redux'
    })
  })

  it('toggleTodo should create TOGGLE_TODO action', () => {
    expect(actions.toggleTodo(1)).toEqual({
      type: 'TOGGLE_TODO',
      id: 1
    })
  })
})

expect creates an expectation
of its argument

import * as actions from './index'

describe('todo actions', () => {
  it('addTodo should create ADD_TODO action', () => {
    expect(actions.addTodo('Use Redux')).toEqual({
      type: 'ADD_TODO',
      id: 0,
      text: 'Use Redux'
    })
  })

  it('toggleTodo should create TOGGLE_TODO action', () => {
    expect(actions.toggleTodo(1)).toEqual({
      type: 'TOGGLE_TODO',
      id: 1
    })
  })
})

Expectations are chained with matchers


import * as actions from './index'

describe('todo actions', () => {
  it('addTodo should create ADD_TODO action', () => {
    expect(actions.addTodo('Use Redux')).toEqual({
      type: 'ADD_TODO',
      id: 0,
      text: 'Use Redux'
    })
  })

  it('toggleTodo should create TOGGLE_TODO action', () => {
    expect(actions.toggleTodo(1)).toEqual({
      type: 'TOGGLE_TODO',
      id: 1
    })
  })
})
  • There is a lot of matchers
    • toBe()
    • toEqual()
    • toContain()
    • toBeLessThan()
    • toThrowError()
    • ...

https://facebook.github.io/jest/docs/expect.html

Let's write some!

src/reducers/index.js
const todos = (state = [], action) => {
  switch (action.type) {
    default:
      return state
  }
}
 
export default todos
  • Does not do much
  • But already misses a test
    • (actually 2)

Unknown actions

src/reducers/index.spec.js
import todos from './index'

describe('todos reducer', () => {
  it('ignores unknown actions', () => {
    const unknownAction = { type: undefined }
    const initialState = []
    expect(todos(initialState, unknownAction)).toBe(initialState)
  })
})

ADD_TODO actions

  • The todos reducer should handle 'ADD_TODO' actions:
    • create and append an {id, text, completed} object to the state
    • completed should be false by default
  • Write the test (not the code)

ADD_TODO actions

src/reducers/index.spec.js
import todos from './index'
 
describe('todos reducer', () => {
[...]
it('creates a new todo on ADD_TODO actions', () => { const action = { type: 'ADD_TODO', id: 42, text: 'whatever' } const initialState = [ { id: 1, text: 'foo', completed: true } ] expect(todos(initialState, action)).toEqual([ { id: 1, text: 'foo', completed: true }, { id: 42, text: 'whatever', completed: false } ]) }) })

ADD_TODO actions

git checkout add-todo

ADD_TODO actions

src/reducers/index.js
const todo = (state, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        id: action.id,
        text: action.text,
        completed: false
      }
    default:
      return state
  }
}
 
const todos = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        todo(undefined, action)
      ]
    default:
      return state
  }
}
 
export default todos

ADD_TODO actions

TOGGLE_TODO actions

  • The todos reducer should handle 'TOGGLE_TODO' actions:
    • Find the todo with the corresponding id
    • Toggle its completed attribute
  • Write the test (not the code)

TOGGLE_TODO actions

src/reducers/index.spec.js
import todos from './index'

describe('todos reducer', () => {
[...]
it('toggle the right todo on TOGGLE_TODO actions', () => { const action = { type: 'TOGGLE_TODO', id: 2 } const initialState = [ { id: 1, text: 'foo', completed: false }, { id: 2, text: 'bar', completed: false } ] expect(todos(initialState, action)).toEqual([ { id: 1, text: 'foo', completed: false }, { id: 2, text: 'bar', completed: true } ]) }) })

TOGGLE_TODO actions

git checkout add-todo

TOGGLE_TODO actions

Find the error

TOGGLE_TODO actions

src/reducers/index.js
const todo = (state, action) => {
  switch (action.type) {
[...]
case 'TOGGLE_TODO': if (state.id === action.id) { return state } return { ...state, completed: !state.completed } default: return state } }
[...]

UI Testing

Traditional UI testing

  1. Render the DOM
  2. Methodically query it and check it


describe('<MyComponent />', () => {
  it('renders three .foo', () => {
    // Create the component.
    var component = React.addons.TestUtil.renderIntoDocument(MyComponent)
    // Fetch its DOM node.
    var componentElement = ReactDOM.findDOMNode(component)
    // Check the .foo.
    expect(componentElement.querySelectorAll('.foo').length).toBe(3)
  })
})

Traditional UI testing

  • Powerful
  • Can be very long to code

Traditional UI testing

describe('<MyComponent />', () => {
  it('renders three .foo', () => {
    // Create the component.
    var component = React.addons.TestUtil.renderIntoDocument(MyComponent)
    // Fetch its DOM node.
    var componentElement = ReactDOM.findDOMNode(component)
    // Check the .foo.
    expect(componentElement.querySelectorAll('.foo').length).toBe(3)
  })
  it('renders an .icon-star', () => {
    var component = React.addons.TestUtil.renderIntoDocument(MyComponent)
    var componentElement = ReactDOM.findDOMNode(component)
    expect(componentElement.querySelectorAll('.icon-star').length).toBe(1)
  })
  it('renders this', () => { /* ... */ });
  it('and that', () => { /* ... */ });
  it('and a chicken', () => { /* ... */ });
})

Snapshot Testing to the rescue

  1. Render the DOM
  2. Match it with a snapshot


describe('<MyComponent />', () => {
  it('renders correctly', () => {
    const tree = renderer.create(
      <Component />
    ).toJSON()
    expect(tree).toMatchSnapshot()
  })
})

Snapshot Testing

  • Way faster to code
  • Ensure components do not change unexpectedly
  • Generally breaks the TDD approach

<Todo>

src/components/Todo.spec.js
import React from 'react'
import renderer from 'react-test-renderer'
import Todo from './Todo'
 
describe('<Todo />', () => {
  it('renders correctly when completed', () => {
    const tree = renderer.create(
       <Todo
         completed
         text="bar"
         onClick={()=>{}}
       />
    ).toJSON()
    expect(tree).toMatchSnapshot()
  })
 it('renders correctly when not completed', () => {
   // ...
 })
})

Snapshots

  • Snapshots for new tests are automatically created
    • ./__snapshots__/
  • If old snapshots do no match anymore:
    • Jest complains
    • Allows you to update the snapshots
  • Snapshots must be committed to VCS (e.g. git)

<Todo>

src/components/Todo.spec.js
import React from 'react'
import renderer from 'react-test-renderer'
import Todo from './Todo'
 
describe('<Todo />', () => {
  it('renders correctly when completed', () => {
    const tree = renderer.create(
       <Todo
         completed
         text="bar"
         onClick={()=>{}}
       />
    ).toJSON()
    expect(tree).toMatchSnapshot()
  })
 it('renders correctly when not completed', () => {
   // Try it!
 })
})

Simulating events

src/components/Todo.spec.js
import React from 'react'
import renderer from 'react-test-renderer'
import { shallow } from 'enzyme'
import Todo from './Todo'
 
describe('<Todo />', () => {
[...]
it('calls onClick when clicked', () => { const wrapper = shallow( <Todo completed text="foo" onClick={()=>{}} /> ).simulate('click', 'click arguments') // Now what? }) })

http://airbnb.io/enzyme

  • How to test if a function is called?
  • How to test how it is called?

Mocking

  • Fake something
    • E.g. function, object, etc.
  • Provide it to a tested module
  • Spy how it is handled

<Todo> onClick

src/components/Todo.spec.js
import React from 'react'
import renderer from 'react-test-renderer'
import { shallow } from 'enzyme'
import Todo from './Todo'
 
describe('<Todo />', () => {
[...]
it('calls onClick when clicked', () => { // Create a mock function for the onClick handler. const onClick = jest.fn() const wrapper = shallow( <Todo completed text="foo" onClick={onClick} /> ).simulate('click', 'click args') // Check how the onClick mock has been called. expect(onClick.mock.calls).toEqual([ ['click args'] ]) })

<TodoList> rendering

  • <TodoList> is a container
    • Created with Redux's connect
    • Needs access to a store

<TodoList> rendering

src/containers/TodoList.spec.js
import React from 'react'
import renderer from 'react-test-renderer'
import configureMockStore from 'redux-mock-store'
import TodoList from './TodoList'
 
const mockStore = configureMockStore()
 
describe('<TodoList />', () => {
  it('renders correctly', () => {
    // Define the initial state.
    const state = [
      { id: 1, text: 'foo', completed: false },
      { id: 2, text: 'bar', completed: true }
    ]
    // Create the store with this state.
    const store = mockStore(state)
    // Renders <TodoList> with this store.
    const tree = renderer.create(
      <TodoList
        store={store}
      />
    ).toJSON()
    expect(tree).toMatchSnapshot()
  })
})
  • It works!
  • Still, any problem with this?
src/containers/TodoList.js
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import Todo from '../components/Todo'
import { toggleTodo } from '../actions'
 
const TodoList = ({ todos, onTodoClick }) => (
  <ul>
    {todos.map(todo =>
      <Todo
        key={todo.id}
        {...todo}
        onClick={() => onTodoClick(todo.id)}
      />
    )}
  </ul>
)
[...]

Is it properly isolated?

  • Our <TodoList> test depends on <Todo>
    • If <Todo> breaks, it breaks
    • If <Todo> changes, it must change too
  • Usually → dependency injection
  • But Jest supports import mocking!

<TodoList> rendering

src/containers/TodoList.spec.js
import React from 'react'
import renderer from 'react-test-renderer'
import TodoList from './TodoList'
import configureMockStore from 'redux-mock-store'
 
// Mock the Todo module and replace its export by a <Todo> tag.
jest.mock('../components/Todo', () => 'Todo')
// Also mock the toggleTodo action from the actions module.
jest.mock(
  '../actions',
  () => ({ toggleTodo: jest.fn() })
);


const mockStore = configureMockStore()

describe('<TodoList />', () => {
[...]
})

<TodoList> on <Todo> click

Test the behavior of <TodoList>
when a <Todo> is clicked