简体   繁体   English

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

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

I'm trying to refactor a unit test to isolate a service that calls an API using axios from the component calling the service.我正在尝试重构一个单元测试,以从调用该服务的组件中隔离一个使用 axios 调用 API 的服务。

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:我可以测试 ObjectList 是否按照 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.作为一项主要的学术练习,我试图弄清楚如何模拟 SomeService 而不是 axios,这就是事情出错的地方,因为我认为我对传递的内部结构了解不够。

For example, I figured since SomeService just returns the axios response, I could similarly mock SomeService, sort of like this:例如,我认为由于 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.

This fails with an error: Error: Uncaught [TypeError: Cannot read property 'then' of undefined] , and the error traces back to this line from ObjectList :这失败并出现错误: Error: Uncaught [TypeError: Cannot read property 'then' of undefined] ,并且错误从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?我具体需要模拟什么,以便ObjectList组件可以从SomeService获得它需要的东西来设置它的 state?

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:使用 jest 文档中建议的不同方法进行了一些试验和错误之后,似乎唯一可行的方法是使用模块工厂参数调用jest.mock() ,如下所示:

// 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.使用mockResolvedValue()不起作用,因为我无法将 .then( .then()链接出来。

If this leads anyone to a more elegant or idiomatic solution, I'd welcome other answers.如果这会导致任何人找到更优雅或惯用的解决方案,我会欢迎其他答案。

The problem with mocking class instances is that it may be difficult to reach class instance and its methods without having a reference. mocking class 实例的问题在于,在没有参考的情况下可能难以到达 class 实例及其方法。 Since someService is local to component module, it can't be accessed directly.由于someService是组件模块的本地服务,因此无法直接访问。

Without specific mock, jest.mock('./SomeService') relies on class automatic mock that works in unspecified ways.如果没有特定的模拟, jest.mock('./SomeService')依赖于以未指定方式工作的class 自动模拟 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.该问题表明,模拟 class 的不同实例具有不同的getObjects模拟方法,这些方法不会相互影响,尽管getObjects是原型方法并且符合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.解决方案是不要依赖自动 mocking,而是让它按照预期的方式工作。 A practical way to make mocked method accessible outside class instance is to carry it alongside mocked module.使模拟方法可以在 class 实例之外访问的一种实用方法是将其与模拟模块一起携带。 This way mockGetObjects.mockImplementationOnce will affect existing someService .这样mockGetObjects.mockImplementationOnce会影响现有的someService mockImplementationOnce implies that the method can change the implementation later per test: 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

If the method should have constant mocked implementation, this simplifies the task because the implementation can be specified in jest.mock .如果该方法应该具有恒定的模拟实现,这会简化任务,因为可以在jest.mock中指定实现。 It may still be beneficial to expose mockGetObjects for assertions.为断言公开mockGetObjects可能仍然是有益的。

For posterity, another solution is creating a manual mock within a __mocks__ folder (inspired by Estus Flask's comment and this documentation ).对于后代,另一种解决方案是在__mocks__文件夹中创建手动模拟(受 Estus Flask 的评论和本文档的启发)。

./__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:然后简单的jest.mock('./SomeService')调用与稍后在测试中定义的实现一起工作:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM