Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async component snapshot using Jest and Redux

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();
});
like image 825
Dennis Persson Avatar asked Oct 14 '25 04:10

Dennis Persson


2 Answers

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()
});
like image 116
Andreas Köberle Avatar answered Oct 16 '25 22:10

Andreas Köberle


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

like image 35
Robin Pokorny Avatar answered Oct 16 '25 21:10

Robin Pokorny



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!