异步代码
我们项目中经常也会涉及到异步代码,比如setTimeout、接口请求等都会涉及到异步,那么这些异步代码怎么来进行测试呢?假设我们有一个异步获取数据的函数fetchData:
export function fetchData(cb) {
setTimeout(() => {
cb("res data");
}, 2000);
}
在2秒后通过回调函数返回了一个字符串,我们可以在测试用例的函数中使用一个done的参数,Jest会等done回调后再完成测试:
test("callback", (done) => {
function cb(data) {
try {
expect(data).toBe("res data");
done();
} catch (error) {
done();
}
}
fetchData(cb);
});
我们将一个回调函数传入fetchData,在回调函数中对返回的数据进行断言,在断言结束后需要调用done;如果最后没有调用done,那么Jest不知道什么时候结束,就会报错;在我们日常代码中,都会通过promise来获取数据,将我们的fetchData进行一下改写:
export function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("promise data");
}, 2000);
});
}
Jest支持在测试用例中直接返回一个promise,我们可以在then中进行断言:
test("promise callback", () => {
return fetchData().then((res) => {
expect(res).toBe("promise data");
});
});
除了直接将fetchData返回,我们也可以在断言中使用.resolves/.rejects 匹配符,Jest也会等待promise结束:
test("promise callback", () => {
return expect(fetchData()).resolves.toBe("promise data");
});
除此之外,Jest还支持async/await,不过我们需要在test的匿名函数加上async修饰符表示:
test("async/await callback", async () => {
const data = await fetchData();
expect(data).toBe("promise data");
});
全局挂载与卸载
全局挂载和卸载有点类似Vue-Router的全局守卫,在每个导航触发前和触发后做一些操作;在Jest中也有,比如我们需要在每个测试用例前初始化一些数据,或者在每个测试用例之后清除数据,就可以使用beforeEach和afterEach:
let cityList = []
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test("city data has suzhou", () => {
expect(cityList).toContain("suzhou")
})
test("city data has shanghai", () => {
expect(cityList).toContain("suzhou")
})
这样,每个测试用例进行测试前都会调用init,每次结束后都会调用clear;我们有可能会在某些test中更改cityList的数据,但是在beforeEach进行初始化的操作后,每个测试用例获取的cityList数据就保证都是相同的;和上面一节异步代码一样,在beforeEach和afterEach我们也可以使用异步代码来进行初始化:
let cityList = []
beforeEach(() => {
return initializeCityDatabase().then((res)=>{
cityList = res.data
});
});
//或者使用async/await
beforeEach(async () => {
cityList = await initializeCityDatabase();
});
和beforeEach和afterEach相对应的就是beforeAll和afterAll,区别就是beforeAll和afterAll只会执行一次;beforeEach和afterEach默认会应用到每个test,但是我们可能希望只针对某些test,我们可以通过describe将这些test放到一起,这样就只应用到describe块中的test:
beforeEach(() => {
// 应用到所有的test
});
describe("put test together", () => {
beforeEach(() => {
// 只应用当前describe块中的test
});
test("test1", ()=> {})
test("test2", ()=> {})
});
模拟函数
在项目中,一个模块的函数内常常会去调用另外一个模块的函数。在单元测试中,我们可能并不需要关心内部调用的函数的执行过程和结果,只想知道被调用模块的函数是否被正确调用,甚至会指定该函数的返回值,因此模拟函数十分有必要。
如果我们正在测试一个函数forEach,它的参数包括了一个回调函数,作用在数组上的每个元素:
export function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
为了测试这个forEach,我们需要构建一个模拟函数,来检查模拟函数是否按照预期被调用了:
test("mock callback", () => {
const mockCallback = jest.fn((x) => 42 + x);
forEach([0, 1, 2], mockCallback);
expect(mockCallback.mock.calls.length).toBe(3);
expect(mockCallback.mock.calls[0][0]).toBe(0);
expect(mockCallback.mock.calls[1][0]).toBe(1);
expect(mockCallback.mock.calls[2][0]).toBe(1);
expect(mockCallback.mock.results[0].value).toBe(42);
});
我们发现在mockCallback有一个特殊的.mock属性,它保存了模拟函数被调用的信息;我们打印出来看下:
它有四个属性:
· calls:调用参数
· instances:this指向
· invocationCallOrder:函数调用顺序
· results:调用结果
在上面属性中有一个instances属性,表示了函数的this指向,我们还可以通过bind函数来更改我们模拟函数的this:
test("mock callback", () => {
const mockCallback = jest.fn((x) => 42 + x);
const obj = { a: 1 };
const bindMockCallback = mockCallback.bind(obj);
forEach([0, 1, 2], bindMockCallback);
expect(mockCallback.mock.instances[0]).toEqual(obj);
expect(mockCallback.mock.instances[1]).toEqual(obj);
expect(mockCallback.mock.instances[2]).toEqual(obj);
});
通过bind更改函数的this之后,我们可以用instances来进行检测;模拟函数可以在运行时将返回值进行注入:
const myMock = jest.fn();
// undefined
console.log(myMock());
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce("x")
.mockReturnValue(true);
//10 x true true
console.log(myMock(), myMock(), myMock(), myMock());
myMock.mockReturnValueOnce(null);
// null true true
console.log(myMock(), myMock(), myMock());
我们第一次执行myMock,由于没有注入任何返回值,然后通过mockReturnValueOnce和mockReturnValue进行返回值注入,Once只会注入一次;模拟函数在连续性函数传递返回值时使用注入非常的有用:
const filterFn = jest.fn();
filterFn.mockReturnValueOnce(true).mockReturnValueOnce(false);
const result = [2, 3].filter((num) => filterFn(num));
expect(result).toEqual([2]);
我们还可以对模拟函数的调用情况进行断言:
const mockFunc = jest.fn();
// 断言函数还没有被调用
expect(mockFunc).not.toHaveBeenCalled();
mockFunc(1, 2);
mockFunc(2, 3);
// 断言函数至少调用一次
expect(mockFunc).toHaveBeenCalled();
// 断言函数调用参数
expect(mockFunc).toHaveBeenCalledWith(1, 2);
expect(mockFunc).toHaveBeenCalledWith(2, 3);
// 断言函数最后一次的调用参数
expect(mockFunc).toHaveBeenLastCalledWith(2, 3);
除了能对函数进行模拟,Jest还支持拦截axios返回数据,假如我们有一个获取用户的接口:
// /src/api/users
const axios = require("axios");
function fetchUserData() {
return axios
.get("/user.json")
.then((resp) => resp.data);
}
module.exports = {
fetchUserData,
};
现在我们想要测试fetchUserData函数获取数据但是并不实际请求接口,我们可以使用jest.mock来模拟axios模块:
const users = require("../api/users");
const axios = require("axios");
jest.mock("axios");
test("should fetch users", () => {
const userData = {
name: "aaa",
age: 10,
};
const resp = { data: userData };
axios.get.mockResolvedValue(resp);
return users.fetchUserData().then((res) => {
expect(res).toEqual(userData);
});
});
一旦我们对模块进行了模拟,我们可以用get函数提供一个mockResolvedValue方法,以返回我们需要测试的数据;通过模拟后,实际上axios并没有去真正发送请求去获取/user.json的数据。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理