How can I mock a service within a React component to isolate unit tests in jest?

孤街浪徒 提交于 2021-01-28 13:08:55

问题


I'm trying to refactor a unit test to isolate a service that calls an API using axios from the component calling the service.

The service is for the moment really simple:

import axios from 'axios'

export default class SomeService {
  getObjects() {
    return axios.get('/api/objects/').then(response => response.data);
  }
}

Here's a snippet of the component that calls the service:

const someService = new SomeService();

class ObjectList extends Component {
  state = {
    data: [],
  }

  componentDidMount() {
    someService.getObjects().then((result) => {
      this.setState({
        data: result,
      });
    });
  }

  render() {
    // render table rows with object data
  }
}

export default ObjectList

I can test that ObjectList renders data as I'd expect by mocking axios:

// ...
jest.mock('axios')

const object_data = {
  data: require('./test_json/object_list_response.json'),
};

describe('ObjectList', () => {
  test('generates table rows from object api data', async () => {

    axios.get.mockImplementationOnce(() => Promise.resolve(object_data));

    const { getAllByRole } = render(
      <MemoryRouter>
        <table><tbody><ObjectList /></tbody></table>
      </MemoryRouter>
    );

    await wait();

    // test table contents

  });
});

Everything passes without issue. As a mostly academic exercise, I was trying to figure out how to mock SomeService instead of axios, which is where things went awry because I think I don't understand enough about the internals of what's getting passed around.

For example, I figured since SomeService just returns the axios response, I could similarly mock SomeService, sort of like this:

// ...
const someService = new SomeService();

jest.mock('./SomeService')

const object_data = {
  data: require('./test_json/object_list_response.json'),
};

describe('ObjectList', () => {
  test('generates table rows from object api data', async () => {

    someService.getObjects.mockImplementationOnce(() => Promise.resolve(object_data))

// etc.

This fails with an error: Error: Uncaught [TypeError: Cannot read property 'then' of undefined], and the error traces back to this line from ObjectList:

someService.getObjects().then((result) => {

What specifically do I need to mock so that the ObjectList component can get what it needs to from SomeService to set its state?


回答1:


The problem with mocking class instances is that it may be difficult to reach class instance and its methods without having a reference. Since someService is local to component module, it can't be accessed directly.

Without specific mock, jest.mock('./SomeService') relies on class automatic mock that works in unspecified ways. The question shows that different instances of mocked class have different getObjects mocked methods that don't affect each other, despite getObjects is prototype method and conforms to new SomeService().getObjects === new SomeService().getObjects in unmocked class.

The solution is to not rely on automatic mocking but make it work the way it's expected. A practical way to make mocked method accessible outside class instance is to carry it alongside mocked module. This way mockGetObjects.mockImplementationOnce will affect existing someService. mockImplementationOnce implies that the method can change the implementation later per test:

import { mockGetObjects }, SomeService from './SomeService';

jest.mock('./SomeService', () => {
  let mockGetObjects = jest.fn();
  return {
    __esModule: true,
    mockGetObjects,
    default: jest.fn(() => ({ getObjects: mockGetObjects }))
  };
});

...

mockGetObjects.mockImplementationOnce(...);
// instantiate the component

If the method should have constant mocked implementation, this simplifies the task because the implementation can be specified in jest.mock. It may still be beneficial to expose mockGetObjects for assertions.




回答2:


After some trial and error playing around with different approaches suggested in the jest documentation, the only thing that seemed to work was calling jest.mock() with the module factory parameter, like so:

// rename data to start with 'mock' so that the factory can use it
const mock_data = {
  data: require('./test_json/object_list_response.json'),
};

jest.mock('./SomeService', () => {
  return jest.fn().mockImplementation(() => {
    return {
      getObjects: () => {
        return Promise.resolve(mock_data).then(response => response.data)
      }
    };
  });
});

// write tests

Using mockResolvedValue() didn't work because I couldn't chain .then() off of it.

If this leads anyone to a more elegant or idiomatic solution, I'd welcome other answers.




回答3:


For posterity, another solution is creating a manual mock within a __mocks__ folder (inspired by Estus Flask's comment and this documentation).

./__mocks__/SomeService.js

export const mockGetObjects = jest.fn()

const mock = jest.fn(() => {
    return {getObjects: mockGetObjects}
})

export default mock

Then the plain jest.mock('./SomeService') call works with the implementation later being defined in the test:

mockGetObjects.mockImplementationOnce(() => {
  return Promise.resolve(object_data).then(response => response.data)
})


来源:https://stackoverflow.com/questions/63286972/how-can-i-mock-a-service-within-a-react-component-to-isolate-unit-tests-in-jest

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!