4

I find Jest to be very useful when testing my Redux React application. However, there are many examples of how to test async action creators, but I can't really figure out how to snapshot async components.

What I would like to do is something similar to the hovered link example from Facebook's own tutorial. They call a props function onMouseEnter() and subsequently take the snapshot. Is there an easy way to do that if onMouseEnter() dispatches an async action created with Redux Thunk?

This is how my thunk looks like, which uses axios.

  // test-api.js

  export function getLinkInfo() {
  return function(dispatch) {
    return axios.get('/api/get-link-info')
    .then(response => {
        dispatch(getLinkInfoSuccess(response.data));
        return response;
    });
  };
}

Here comes my own Link component.

import React from 'react';
import { connect } from 'react-redux';
import * as api from '../../api/test-api';

class Link extends React.Component {
  render() {
    return (
      <a href='#' onMouseEnter={this.props.getLinkInfo}>
        Hover me
      </a>
      <div>{this.props.linkInfo}</div>
    );
  }
}

const mapDispatchToProps = function(dispatch) {
  return {
    getLinkInfo: function() {
      dispatch(api.getLinkInfo());
    }
  }
}

const mapStateToProps = function(store) {
  return {
    linkInfo: store.getIn(['testState', 'linkInfo'], "")
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Link);

And at last the test file.

import React from 'react';
import Link from '../link';
import renderer from 'react-test-renderer';

test('Link changes linkInfo when hovered', () => {
  const component = renderer.create(
    <Link></Link>
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // manually trigger the callback
  tree.props.onMouseEnter();
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});
David Schumann
  • 13,380
  • 9
  • 75
  • 96
Dennis Persson
  • 843
  • 7
  • 23

2 Answers2

3

The problem is that when you like to test async stuff you need the instance of the promise in your test, either to return it from test, so jest knows about it and can wait for it, or use async await inside the test it self (docs).

What you could do, is to mock the api inside of your test:

import {getLinkInfo} from 'path/to/the/api' jest.mock('path/to/the/api', () = > ({ getLinkInfo: jest.fn() }))

This will overwrite the module with an object that has a spy for getLinkInfo. Then import the module so you can set the actual implementation of the spy in your test.

test('Link changes linkInfo when hovered', () = > {
  //create a new promise that can be returned from your test
  const p = new Promise((resolve) = > {
    //set the spy to make the request and resolve the promise
    getInfo.mockImplementation(function (dispatch) {
      return axios.get('/api/get-link-info')
        .then(response = > {
          dispatch(getLinkInfoSuccess(response.data));
          resolve(response);
        });
    };)
  };)
  const component = renderer.create(
    <Link></Link>
  );
  let tree = component.toJSON();
  expect(tree)
    .toMatchSnapshot();
  // manually trigger the callback
  tree.props.onMouseEnter();
  return p.then(() = > {
    tree = component.toJSON();
    expect(tree)
      .toMatchSnapshot()
  })
});

While this may solve you actual problem, I would suggest to not run your test like this, with a call to your real API, but also mock the request it self. First your test will be much faster and it does not dependent to some running backend.

The point is that you want to test your React component as a unit, so it does not care what happened after it called getLinkInfo. These are details for the unit test of getLinkInfo. All that your component knows is, it calls getLinkInfo pass in an callback and this callback will be called sometimes. When it is called and what happened in between is not part of the responsibility of the component. If you think about the test like this, the simplest solution is to call the callback immediately.

test('Link changes linkInfo when hovered', () = > {
  getInfo.mockImplementation(function (dispatch) {
    dispatch({
      some: 'Data'
    });
  };)
  const component = renderer.create(
    <Link></Link>
  );
  let tree = component.toJSON();
  expect(tree)
    .toMatchSnapshot();
  // manually trigger the callback
  tree.props.onMouseEnter();
  tree = component.toJSON();
  expect(tree).toMatchSnapshot()
});
David Schumann
  • 13,380
  • 9
  • 75
  • 96
Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
  • Thanks for the solution. The reason to why I wanted to test against the backend and through the whole Redux flow in a single test was to get a substitution for automatic browser GUI testing that could run in the background. So I basically would want a wait method in Jest that could wait for a component to rerender. Would that really be such a bad idea? – Dennis Persson Mar 15 '17 at 21:53
  • 1
    Its not a bad idea to have this kind of test but there are better tools for integration test like selenium or cypress.io – Andreas Köberle Mar 15 '17 at 23:05
1

In the tutorial, they have a stateful component. That requires performing such ‘gymnastics’.

For a pure, stateless component, like you have, one should only test two things:

  1. that it renders correctly with any combination of props,
  2. that the correct handler is called on a certain event.

However, you only export the HOC that connect produces. You can solve this by exporting both (and also mapDispatchToProps and mapStateToProps). Or, alternatively, by mocking connect so it returns the original component for the test.

The file would look like this:

import …

export class Link extends React.Component {
    …
}

export const mapDispatchToProps = …

export const mapStateToProps = …

export default connect(mapStateToProps, mapDispatchToProps)(Link);

And the test:

import …
import { Link, mapDispatchToProps, mapStateToProps } from './Link'

test('renders correctly', () => {
  const tree = renderer.create(
    <Link linkInfo="Link info" />
  ).toJSON()

  expect(tree).toMatchSnapshot()
})

test('calls getLinkInfo', () => {
  const getLinkInfo = jest.fn()

  const tree = renderer.create(
    <Link getLinkInfo={getLinkInfo} />
  )

  tree.props.onMouseEnter()

  expect(getLinkInfo).toHaveBeenCalled()
})

test('mapDispatchToProps', () => … )
test('mapStateToProps', () => … )

This is a complete test of the pure component.


The second part of your question is about testing an async action creator. The tricky part is axios. Where does it come from? I assume you import it on the top. So you would have to mock it—ugh, that can get messy pretty soon.

There's a lesser know extraArgument you can pass to redux thunk. This can work as a pure dependency injection, which makes the action creator so easy to test.

Use like this:

const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument({ axios }))
)

Then this dependency (or more if you need) is passed as the third argument to the thunk:

export function getLinkInfo() {
  return function(dispatch, getState, { axios }) {
    return axios.get('/api/get-link-info')
    .then(response => {
        dispatch(getLinkInfoSuccess(response.data));
        return response;
    });
  };
}

Now comes the cool. The test of async action creators:

import * as actions from './actions'

describe('getLinkInfo', () => {
  const action = actions. getLinkInfo()

  const dispatch = jest.fn()
  const getState = () => { … }
  const axios = {
    get: jest.fn(() => Promise.resolve({
      data: {}
    }))
  }

  beforeEach(() => {
    deps.axios.get.mockClear()
  })

  test('fetches info from the server', () => {
    action(dispatch, getState, { axios })

    expect(axios.get).toHaveBeenCalledTimes(1)
    expect(axios.get.mock.calls).toMatchSnapshot()
  })

})

P.S. I show these and some more nice Jest testing patterns here: https://github.com/robinpokorny/jest-example-hannoverjs

David Schumann
  • 13,380
  • 9
  • 75
  • 96
Robin Pokorny
  • 10,657
  • 1
  • 24
  • 32
  • Great examples. Currently I develop this application by myself and don't have so much time, and I feel that writing tests for components, action creators and reducers would be time consuming (or at least very WET). But maybe I should use Selenium Webdriver instead. The important thing for me is to have regression tests with great coverage. – Dennis Persson Mar 15 '17 at 21:54