测试是关于隔离你想要测试的东西。要做到这一点,我们可能需要模拟某个模块导出的一些函数。此外,我们可能不希望模拟该模块导出的某些函数。
考虑一下下面这个假想的例子。首先,我们定义了一个模块,它有一个命名的导出,api ,执行一个fetch 请求。然后,我们定义了一个名为returnSomething 的出口,它返回一个字符串。
someModule.js
export const api = async (endpoint) => {
return await fetch(endpoint).then((res) => res.json());
};
export const returnSomething = () => {
return 'something';
};
接下来,我们在一个getData 函数中导入并调用这两个函数,我们稍后要测试一下。
getData.js
import { api, returnSomething } from './someModule';
export const getData = async () => {
const data = await api('/some-endpoint');
return {
data: data,
somethingElse: returnSomething(),
};
};
现在是有趣的部分:测试!
所以现在我们要对我们的app 函数进行单元测试:我们要测试我们的getData 函数的返回值。我们开始编写测试。
getData.test.js
import { getData } from './getData';
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: '???',
somethingElse: 'something',
});
});
});
但是我们不知道data 会是什么,因为我们的fetch 请求是在我们单元测试的边界之外。好消息是,我们可以用Jest模拟来解决这个问题。
import { getData } from './getData';
jest.mock('./someModule', () => ({
api: () => Promise.resolve('foo'),
}));
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: 'foo',
somethingElse: 'something',
});
});
});
然而,如果我们尝试这样做,我们仍然得到了错误!这是因为我们在模拟时,没有考虑到这一点。
TypeError: (0 , _utils.returnSomething) is not a function
这是因为我们正在模拟整个someModule 模块。由于我们没有模拟从它导出的returnSomething 函数,所以它没有被定义!
为了解决这个问题,我们可以使用jest.requireActual 来要求实际的模块,并在周围保留我们想要的函数。
jest.mock('./someModule', () => ({
returnSomething: jest.requireActual('./someModule').returnSomething,
api: () => Promise.resolve('foo'),
}));
这就可以了!但如果我们的模块有很多命名的输出,这就变得很麻烦了。因此,让我们使用传播操作符来复制模块中的所有实际输出,然后只覆盖我们想要覆盖的那些。
jest.mock('./someModule', () => ({
...jest.requireActual('./someModule'),
api: () => Promise.resolve('foo'),
}));
完美!我们的最终代码是这样的。我们的最终代码是这样的,现在我们有了一个漂亮的、孤立的、通过的测试。
import { getData } from './getData';
jest.mock('./someModule', () => ({
...jest.requireActual('./someModule'),
api: () => Promise.resolve('foo'),
}));
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: 'foo',
somethingElse: 'something',
});
});
});
对被模拟的模块进行断言
我们可能想对被模拟的模块进行额外的断言。这不是一个问题。
比方说,我们想断言api 方法正在被调用。我们可以通过确保api 本身是一个jest mock来做到这一点。如果我们愿意的话,我们仍然可以让它返回一个Promise,并与"foo" 进行解析。
import { getData } from './getData';
// Make sure to import api to test it
import { api } from './someModule';
jest.mock('./someModule', () => ({
...jest.requireActual('./someModule'),
api: jest.fn().mockResolvedValue('foo'),
}));
describe('getData', () => {
it('returns an object', async () => {
const result = await getData();
expect(result).toEqual({
data: 'foo',
somethingElse: 'something',
});
});
it('call the api function', async () => {
const result = await getData();
expect(api).toHaveBeenCalled();
});
});
这样就可以了。
如果api出现返回未定义,请注意
如果你从这个测试中得到错误,因为api 似乎返回了undefined ,这可能是jest在默认情况下,在测试之间清除了mock。
为了确保这种情况不会发生,你需要在你的jest配置中添加以下内容。
"jest": {
"resetMocks": false
}
然后,你的测试就应该通过了!如果你在全局范围内禁用了这个选项,请确保在测试之间手动重置mocks。
结论
如果你能想出如何测试你想要的东西,并模拟出你不想要的东西,那么测试是很有趣的!