简介
Unit tests 是由软件开发人员编写和运行的自动化测试,以确保应用程序的一部分符合其设计并按预期运行。就好像我们在谈论面向对象编程一样,一个单元通常是一个完整的接口,例如一个类,但也可以是一个单独的方法。
单元测试的目标是隔离程序的每个部分并显示各个部分是正确的。因此,如果我们考虑您的 ApiService.makeApiCall 函数:
static makeApiCall(
url: string,
normalizeCallback: (d: ResponseData) => ResponseData | null,
callback: (d: any) => any
) {
return ApiClient.get(url)
.then((res: any) => {
callback(normalizeCallback(res.data));
})
.catch(error => {
console.error(error);
});
}
我们可以看到它有一个外部资源调用ApiClient.get,它应该是mocked。在这种情况下模拟 HTTP 请求并不完全正确,因为 ApiService 没有直接使用它们,在这种情况下,您的单元变得比预期的更广泛。
嘲讽
Jest 框架提供了很好的mocking 机制,Omair Nabiel 的例子是正确的。但是,我更喜欢不仅使用预定义的数据对函数进行存根,而且还要检查存根函数是否被调用了预期的次数(所以使用真正的模拟)。所以完整的模拟示例如下所示:
/**
* Importing `ApiClient` directly in order to reference it later
*/
import ApiClient from './apiClient';
/**
* Mocking `ApiClient` with some fake data provider
*/
const mockData = {};
jest.mock('./apiClient', function () {
return {
get: jest.fn((url: string) => {
return Promise.resolve({data: mockData});
})
}
});
这允许向您的测试示例添加额外的断言:
it('should call api client method', () => {
ApiService.makeApiCall('test url', (data) => data, (res) => res);
/**
* Checking `ApiClient.get` to be called desired number of times
* with correct arguments
*/
expect(ApiClient.get).toBeCalledTimes(1);
expect(ApiClient.get).toBeCalledWith('test url');
});
阳性测试
所以,只要我们弄清楚什么以及如何模拟数据,我们就可以找出我们应该测试什么。好的测试应该包括two situations:正面测试 - 通过提供有效数据来测试系统和负面测试 - 通过提供无效数据来测试系统。依我拙见,应该添加第三个分支 - 边界测试 - 专注于被测软件的边界或限制条件的测试。如果您对其他类型的测试感兴趣,请参阅此Glossary。
makeApiCall 方法的正向测试流应该调用normalizeCallback 和callback 方法,因此我们可以编写这个测试如下(但是,给猫剥皮的方法不止一种):
it('should call callbacks consequently', (done) => {
const firstCallback = jest.fn((data: any) => {
return data;
});
const secondCallback = jest.fn((data: any) => {
return data;
});
ApiService.makeApiCall('test url', firstCallback, secondCallback)
.then(() => {
expect(firstCallback).toBeCalledTimes(1);
expect(firstCallback).toBeCalledWith(mockData);
expect(secondCallback).toBeCalledTimes(1);
expect(secondCallback).toBeCalledWith(firstCallback(mockData));
done();
});
});
请注意此测试中的几件事:
- 我正在使用done 回调让我们知道测试已经完成,因为这个测试的异步性质
- 我正在使用mockData 变量,ApiClient.get 的数据被模拟了,所以我检查回调是否有正确的值
- mockData 和类似的变量应该从 mock 开始。否则 Jest 将不允许将其从模拟中取出 scope
负面测试
测试的否定方式看起来非常相似。 ApiClient.get 方法应该抛出错误,ApiService 应该处理它并放入 console。另外,我正在检查是否没有调用任何回调。
import ApiService from './api.service';
const mockError = {message: 'Smth Bad Happened'};
jest.mock('./apiClient', function () {
return {
get: jest.fn().mockImplementation((url: string) => {
console.log('error result');
return Promise.reject(mockError);
})
}
});
describe( 't1', () => {
it('should handle error', (done) => {
console.error = jest.fn();
const firstCallback = jest.fn((data: any) => {
return data;
});
const secondCallback = jest.fn((data: any) => {
return data;
});
ApiService.makeApiCall('test url', firstCallback, secondCallback)
.then(() => {
expect(firstCallback).toBeCalledTimes(0);
expect(secondCallback).toBeCalledTimes(0);
expect(console.error).toBeCalledTimes(1);
expect(console.error).toBeCalledWith(mockError);
done();
});
});
});
边界测试
边界测试在您的情况下可能存在争议,但只要(根据您的类型定义normalizeCallback: (d: ResponseData) => ResponseData | null)第一个回调可以返回null,检查是否成功转移到第二个回调可能是一个好习惯任何错误或异常。我们可以稍微重写我们的第二个测试:
it('should call callbacks consequently', (done) => {
const firstCallback = jest.fn((data: any) => {
return null;
});
const secondCallback = jest.fn((data: any) => {
return data;
});
ApiService.makeApiCall('test url', firstCallback, secondCallback)
.then(() => {
expect(firstCallback).toBeCalledTimes(1);
expect(firstCallback).toBeCalledWith(mockData);
expect(secondCallback).toBeCalledTimes(1);
done();
});
});
测试异步代码
关于测试异步代码,您可以阅读综合文档here。主要思想是当你有异步运行的代码时,Jest 需要知道它正在测试的代码何时完成,然后才能继续进行另一个测试。 Jest 提供了三种方法:
-
通过回调
it('the data is peanut butter', done => {
function callback(data) {
expect(data).toBe('peanut butter');
done();
}
fetchData(callback);
});
Jest 会等到 done 回调被调用后再完成测试。如果从未调用过done(),则测试将失败,这正是您想要发生的。
-
通过承诺
如果您的代码使用 Promise,则有一种更简单的方法来处理异步测试。只需从您的测试中返回一个承诺,Jest 将等待该承诺解决。如果 promise 被拒绝,测试将自动失败。
-
async/await 语法
您可以在测试中使用async 和await。要编写异步测试,只需在传递给测试的函数前面使用 async 关键字即可。
it('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
示例
在这里您可以找到一个现成的代码示例
https://github.com/SergeyMell/jest-experiments
如果您有不清楚的地方,请告诉我。
更新 (29.08.2019)
关于你的问题
您好,如何在同一个文件中模拟 ./apiClient 以实现成功和错误?
根据documentation Jest 将自动将jest.mock 调用提升到模块顶部(在任何导入之前)。似乎您可以使用setMock 或doMock 代替,但是,开发人员不时会遇到issues 以这种方式嘲笑。它们可以通过使用require 而不是import 和其他黑客(参见this article)来覆盖,但我不喜欢这种方式。
在这种情况下,对我来说正确的方法是拆分模拟定义和实现,所以你声明这个模块将像这样被模拟
jest.mock('./apiClient', function () {
return {
get: jest.fn()
}
});
但模拟功能的实现会因测试范围而异:
describe('api service success flow', () => {
beforeAll(() => {
//@ts-ignore
ApiClient.get.mockImplementation((url: string) => {
return Promise.resolve({data: mockData});
})
});
...
});
describe('api service error flow', () => {
beforeAll(() => {
//@ts-ignore
ApiClient.get.mockImplementation((url: string) => {
console.log('error result');
return Promise.reject(mockError);
})
});
...
});
这将允许您将所有api service 相关流存储在一个文件中,据我所知,这是您所期望的。
我已经用实现上述所有内容的api.spec.ts 更新了我的github example。请看一下。