如何在 React 组件中模拟服务以隔离单元测试?

IT技术 javascript reactjs unit-testing jestjs
2021-05-01 10:00:25

我正在尝试重构一个单元测试,以将使用 axios 调用 API 的服务与调用该服务的组件隔离开来。

该服务目前非常简单:

import axios from 'axios'

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

这是调用服务的组件的片段:

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

我可以通过模拟 axios 来测试 ObjectList 是否按照我的预期呈现数据:

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

  });
});

一切都过去了,没有问题。作为一个主要的学术练习,我试图弄清楚如何模拟 SomeService 而不是 axios,这就是事情出错的地方,因为我认为我对传递的内容的内部了解不够。

例如,我想既然 SomeService 只返回 axios 响应,我可以类似地模拟 SomeService,有点像这样:

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

这失败并出现错误:Error: Uncaught [TypeError: Cannot read property 'then' of undefined],并且错误可追溯到以下行ObjectList

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

我具体需要模拟ObjectList什么,以便组件可以获得SomeService设置其状态所需的内容?

3个回答

模拟类实例的问题在于,如果没有引用,可能很难访问类实例及其方法。由于someService是组件module的本地,因此不能直接访问。

没有特定的模拟,jest.mock('./SomeService')依赖于 以未指定方式工作的类自动模拟问题表明,模拟类的不同实例具有不同的getObjects模拟方法,它们互不影响,尽管getObjects是原型方法并且符合非new SomeService().getObjects === new SomeService().getObjects模拟类。

解决方案是不依赖自动模拟,而是让它按照预期的方式工作。使模拟方法在类实例之外可访问的一种实用方法是将其与模拟module一起携带。这种方式mockGetObjects.mockImplementationOnce会影响现有的someService. mockImplementationOnce意味着该方法可以稍后在每个测试中更改实现:

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

如果该方法应该具有常量模拟实现,这将简化任务,因为可以在jest.mock. mockGetObjects为断言公开可能仍然是有益的

在使用 jest 文档中建议的不同方法进行一些试验和错误之后,似乎唯一有效的方法是jest.mock()使用module工厂参数调用,如下所示:

// 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

使用mockResolvedValue()不起作用,因为我无法.then()摆脱它。

如果这会让任何人找到更优雅或更惯用的解决方案,我欢迎其他答案。

对于后代,另一种解决方案是在__mocks__文件夹中创建手动模拟(受 Estus Flask 的评论和本文档的启发)。

./__mocks__/SomeService.js

export const mockGetObjects = jest.fn()

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

export default mock

然后普通jest.mock('./SomeService')调用与稍后在测试中定义的实现一起工作:

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