前端抢饭碗系列之Vue项目中如何做单元测试建议收藏
Posted 小马哥说测试
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端抢饭碗系列之Vue项目中如何做单元测试建议收藏相关的知识,希望对你有一定的参考价值。
异步代码
我们项目中经常也会涉及到异步代码,比如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的数据。
最后为方便大家学习测试,特意给大家准备了一份13G的超实用干货学习资源,涉及的内容非常全面。
包括,软件学习路线图,50多天的上课视频、16个突击实战项目,80余个软件测试用软件,37份测试文档,70个软件测试相关问题,40篇测试经验级文章,上千份测试真题分享,还有2021软件测试面试宝典,还有软件测试求职的各类精选简历,希望对大家有所帮助……
关注我公众号:【程序员小濠】即可获取这份资料了!
如果你不想再体验一次自学时找不到资料,没人解答问题,坚持几天便放弃的感受的话,可以加入我们的群:175317069 大家一起讨论交流,里面也有各种软件测试资料和技术交流。
好文推荐
5年经验之谈:月薪3000到30000,测试工程师的变“行”记!
测试工程师,自动化测试工程师,测试开发工程师,这三个岗位分别需要掌握哪些能力和技术栈?
不要让毒鸡汤毁了你,35岁的测试员没有那么可怕,保持专注更重要
以上是关于前端抢饭碗系列之Vue项目中如何做单元测试建议收藏的主要内容,如果未能解决你的问题,请参考以下文章