繁体   English   中英

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

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

我正在尝试重构一个单元测试,以从调用该服务的组件中隔离一个使用 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

我可以测试 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

  });
});

一切顺利通过。 作为一项主要的学术练习,我试图弄清楚如何模拟 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获得它需要的东西来设置它的 state?

使用 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

使用mockResolvedValue()不起作用,因为我无法将 .then( .then()链接出来。

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

mocking class 实例的问题在于,在没有参考的情况下可能难以到达 class 实例及其方法。 由于someService是组件模块的本地服务,因此无法直接访问。

如果没有特定的模拟, jest.mock('./SomeService')依赖于以未指定方式工作的class 自动模拟 该问题表明,模拟 class 的不同实例具有不同的getObjects模拟方法,这些方法不会相互影响,尽管getObjects是原型方法并且符合new SomeService().getObjects === new SomeService().getObjects in unmocked class。

解决方案是不要依赖自动 mocking,而是让它按照预期的方式工作。 使模拟方法可以在 class 实例之外访问的一种实用方法是将其与模拟模块一起携带。 这样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可能仍然是有益的。

对于后代,另一种解决方案是在__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)
})

暂无
暂无

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

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