我有以下(简化的)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中)。
这或多或少是我希望我的测试用例看起来像:
设置mockSalesService以返回在解析时返回预定义测试数据的promise
创建组件
测试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();
});
答案 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
(或其他)before
或beforeEach
处理程序中进行一些设置即可通过模拟调用setSalesService
对象
现在,理想情况下,您希望将SalesService
作为道具传递给您的组件,因为 是对它的输入,并使用{{1}你隐藏了这种依赖性,可能会让你感到悲伤。但是如果你在一个非常嵌套的组件中需要它,或者如果你正在使用路由器或某些东西,那么将它作为道具传递它会变得非常笨拙。
你也可以使用类似context之类的内容,将所有内容保存在React中。
&#34;理想&#34;解决方案就像依赖注入一样,但这不是React AFAIK的选择。
[1]它还可以帮助提供一个单点来序列化远程服务调用,这可能在某些时候需要。