如何在另一个类中模拟异步函数调用

时间:2017-12-12 17:47:20

标签: javascript reactjs unit-testing asynchronous jestjs

我有以下(简化的)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中获取数据,我在一个名为SalesService的类中抽象出来。这个类我想模拟,对于方法fetchSalesData我想指定返回数据(在一个promise中)。

这或多或少是我希望我的测试用例看起来像:

  • 预定义测试数据
  • 导入SalesView
  • 模拟SalesService
  • 设置mockSalesService以返回在解析时返回预定义测试数据的promise

  • 创建组件

  • AWAIT
  • 检查快照

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

我的问题是:

  • 模拟课应该怎么样?
  • 我应该把这个模拟课放在哪里?
  • 我应该如何导入这个模拟类?
  • 我如何判断这个模拟类取代了真正的类?
  • 如何设置模拟类的特定函数的模拟实现?
  • 如何在测试用例中等待承诺得到解决?

我有一个不起作用的例子如下。测试运行器崩溃时出现错误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();
});

3 个答案:

答案 0 :(得分:12)

有时,当测试难以编写时,它会试图告诉我们我们遇到了设计问题。

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

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

答案 1 :(得分:5)

您可以使用new方法抽象SalesService.create()关键字,然后使用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();
});

答案 2 :(得分:2)

一个&#34;丑陋&#34;我以前用过的方法就是做一种穷人的依赖注射。

它基于这样一个事实:您可能不想在每次需要时实例化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作为道具传递给您的组件,因为 是对它的输入,并使用{{1}你隐藏了这种依赖性,可能会让你感到悲伤。但是如果你在一个非常嵌套的组件中需要它,或者如果你正在使用路由器或某些东西,那么将它作为道具传递它会变得非常笨拙。

你也可以使用类似context之类的内容,将所有内容保存在React中。

&#34;理想&#34;解决方案就像依赖注入一样,但这不是React AFAIK的选择。

[1]它还可以帮助提供一个单点来序列化远程服务调用,这可能在某些时候需要。