我有以下(简化)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中)。
这或多或少是我希望我的测试用例看起来像:
测试SalesChart的外观不是这个问题的一部分,我希望使用Enzyme来解决这个问题。我一直在尝试几十种来模拟这个异步调用,但我似乎无法正确地嘲笑它。我在网上找到了以下Jest模拟的例子,但它们似乎没有涵盖这个基本用法。
我的问题是:
我有一个不起作用的例子如下。测试运行器崩溃时出现错误46718663,堆栈跟踪中的最后一行是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();
});
成为合作者而不是内部合作者。
我的意思是,不是在组件中调用SalesService
,而是通过调用代码接受销售服务作为prop。如果你这样做,那么调用代码也可以是你的测试,在这种情况下,你需要做的就是模拟new SalesService()
本身,并返回你想要的任何东西(使用sinon或任何其他模拟库,甚至只是创建一个手动滚动存根)。
您可以使用SalesService
方法抽象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();
});
,而是希望每个应用程序都拥有一个实例,每个人都使用它。在我的情况下,SalesService
需要一些初始配置,我不想每次都重复。[1]
所以我做的是有一个SalesService
文件,看起来像这样:
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
或before
处理程序中进行一些设置,用模拟对象调用beforeEach
。
现在,理想情况下,你想要将setSalesService
作为道具传递给你的组件,因为它是对它的输入,并且通过使用SalesService
你隐藏了这种依赖性并可能导致你的悲伤。但是如果你在一个非常嵌套的组件中需要它,或者如果你正在使用路由器或其他东西,那么将它作为道具传递就变得非常笨拙了。
你可能也会使用像getSalesService
这样的东西,以保持React内部的所有内容。
这种“理想”的解决方案就像依赖注入一样,但这不是React AFAIK的选择。
[1]它还可以帮助提供一个单点来序列化远程服务调用,这可能在某些时候需要。