首页 文章

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

提问于
浏览
21

我有以下(简化)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

  • mock SalesService

  • 设置mockSalesService以返回在解析时返回预定义测试数据的promise

  • 创建组件

  • 等待

  • 检查快照

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

我的问题是:

  • 模拟类应该怎么样?

  • 我应该把这个模拟课放在哪里?

  • 我该如何导入这个模拟类?

  • 如何判断这个模拟类取代了真正的类?

  • 如何设置mock类的特定函数的模拟实现?

  • 如何在测试用例中等待承诺得到解决?

我有一个不起作用的例子如下 . 测试运行器因错误 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 回答

  • 5

    有时,当测试很难写时,它试图告诉我们我们有设计问题 .

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

    我的意思是,不是在组件中调用 new SalesService() ,而是通过调用代码接受销售服务作为prop . 如果你这样做,那么调用代码也可以是你的测试,在这种情况下,你需要做的就是模拟 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();
    });
    
  • 12

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

    它基于这样一个事实:您可能并不真的想在每次需要时实例化 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]它还可以帮助提供一个单点来序列化远程服务调用,这可能在某些时候需要 .

相关问题