【问题标题】:How to mock an asynchronous function call in another class如何模拟另一个类中的异步函数调用
【发布时间】:2017-12-12 17:47:19
【问题描述】:

我有以下(简化的)React 组件。

class SalesView extends Component<{}, State> {
  state: State = {
    salesData: null
  };

  componentDidMount() {
    this.fetchSalesData();
  }

  render() {
    if (this.state.salesData) {
      return <SalesChart salesData={this.state.salesData} />;
    } else {
      return <p>Loading</p>;
    }
  }

  async fetchSalesData() {
    let data = await new SalesService().fetchSalesData();
    this.setState({ salesData: data });
  }
}

安装时,我从一个 API 中获取数据,该 API 已抽象到一个名为 SalesService 的类中。我想模拟这个类,对于 fetchSalesData 方法我想指定返回数据(在一个承诺中)。

这或多或少是我希望我的测试用例的样子:

  • 预定义测试数据
  • 导入销售视图
  • 模拟销售服务
  • 设置 mockSalesService 以返回一个承诺,该承诺在解决时返回预定义的测试数据

  • 创建组件

  • 等待
  • 检查快照

测试 SalesChart 的外观不是这个问题的一部分,我希望使用 Enzyme 来解决这个问题。我一直在尝试很多东西来模拟这个异步调用,但我似乎无法正确地模拟它。我在网上找到了以下 Jest mocking 的示例,但它们似乎没有涵盖这个基本用法。

我的问题是:

  • 模拟类应该是什么样子的?
  • 我应该把这个模拟类放在哪里?
  • 我应该如何导入这个模拟类?
  • 如何判断这个模拟类取代了真实类?
  • 如何设置mock类的具体功能的mock实现?
  • 如何在测试用例中等待 Promise 得到解决?

下面给出了我有一个不起作用的例子。测试运行程序崩溃,错误为throw err;,堆栈跟踪中的最后一行是at process._tickCallback (internal/process/next_tick.js:188:7)

# __tests__/SalesView-test.js
import React from 'react';
import SalesView from '../SalesView';

jest.mock('../SalesService');
const salesServiceMock = require('../SalesService').default;

const weekTestData = [];

test('SalesView shows chart after SalesService returns data', async () => {
  salesServiceMock.fetchSalesData.mockImplementation(() => {
    console.log('Mock is called');
    return new Promise((resolve) => {
      process.nextTick(() => resolve(weekTestData));
    });
  });

  const wrapper = await shallow(<SalesView/>);
  expect(wrapper).toMatchSnapshot();
});

【问题讨论】:

  • 根据您进行 XHR 调用的方式,您可能需要查看 nockaxios-mock-adapter
  • 谢谢!模拟实际的 HTTP 调用很有趣,但我正在寻找一种方法来模拟我自己的具有异步功能的类。他们发出 HTTP 请求是一种特殊情况。

标签: javascript reactjs unit-testing asynchronous jestjs


【解决方案1】:

有时,当一个测试很难编写时,它试图告诉我们我们有一个设计问题。

我认为一个小的重构可以让事情变得更容易 - 让 SalesService 成为合作者而不是内部人员。

我的意思是,不要在组件中调用new SalesService(),而是通过调用代码接受销售服务作为道具。如果你这样做,那么调用代码也可以是你的测试,在这种情况下,你需要做的就是模拟 SalesService 本身,并返回你想要的任何东西(使用 sinon 或任何其他模拟库,甚至只是创建一个手卷存根)。

【讨论】:

    【解决方案2】:

    您可以使用SalesService.create() 方法抽象出new 关键字,然后使用jest.spyOn(object, methodName) 模拟实现。

    import SalesService from '../SalesService ';
    
    test('SalesView shows chart after SalesService returns data', async () => {
    
        const mockSalesService = {
            fetchSalesData: jest.fn(() => {
                return new Promise((resolve) => {
                    process.nextTick(() => resolve(weekTestData));
                });
            })
        };
    
        const spy = jest.spyOn(SalesService, 'create').mockImplementation(() => mockSalesService);
    
        const wrapper = await shallow(<SalesView />);
        expect(wrapper).toMatchSnapshot();
        expect(spy).toHaveBeenCalled();
        expect(mockSalesService.fetchSalesData).toHaveBeenCalled();
    
        spy.mockReset();
        spy.mockRestore();
    });
    

    【讨论】:

      【解决方案3】:

      我过去使用的一种“丑陋”方式是进行一种穷人的依赖注入。

      这是基于这样一个事实,即您可能并不真的想在每次需要时都实例化SalesService,而是希望每个应用程序都拥有一个实例,每个人都使用它。就我而言,SalesService 需要一些我不想每次都重复的初始配置。[1]

      所以我所做的是有一个看起来像这样的services.ts 文件:

      /// In services.ts
      let salesService: SalesService|null = null;
      export function setSalesService(s: SalesService) {
          salesService = s;
      }
      export function getSalesService() {
          if(salesService == null) throw new Error('Bad stuff');
          return salesService;
      }
      

      然后,在我的应用程序的 index.tsx 或类似的地方:

      /// In index.tsx
      // initialize stuff
      const salesService = new SalesService(/* initialization parameters */)
      services.setSalesService(salesService);
      // other initialization, including calls to React.render etc.
      

      然后,您可以在组件中使用 getSalesService 来获取对每个应用程序一个 SalesService 实例的引用。

      当需要进行测试时,您只需在您的mocha(或其他)beforebeforeEach 处理程序中进行一些设置,以使用模拟对象调用setSalesService

      现在,理想情况下,您希望将 SalesService 作为道具传递给您的组件,因为它它的输入,并且通过使用 getSalesService 您隐藏了这个依赖,并可能导致你在路上悲伤。但是如果你在一个非常嵌套的组件中需要它,或者如果你正在使用路由器或类似的东西,那么将它作为道具传递就变得非常笨拙了。

      您也可以使用 context 之类的东西来保持 React 中的所有内容不变。

      对此的“理想”解决方案类似于依赖注入,但这不是 React AFAIK 的选项。


      [1] 它还有助于为序列化远程服务调用提供单点,这在某些时候可能需要。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-09-18
        • 1970-01-01
        • 2019-04-18
        • 1970-01-01
        • 2020-04-02
        • 1970-01-01
        • 2021-04-02
        相关资源
        最近更新 更多