# 2024年,重新探索前端单元测试之路,从入门到精通01-Vitest
# 2024年,重新探索前端单元测试之路,从入门到精通02-Vitest
十四、行为验证
这里主要讲的是对于函数的调用次数,以及函数参数等验证
这里举得例子是登录接口
import { phoneLogin } from "xxx/api";
const state = {
tipString: "",
};
export function login(username: string, password: string) {
phoneLogin(username, password);
}
export function loginV2(username: string, password: string) {
const isLogin = phoneLogin(username, password);
if (isLogin) {
state.tipString = "登录成功拉";
}
}
export function getTip() {
return state.tipString;
}
接下来看看我们是怎么测试这个登录接口的
import { vi, it, expect, describe } from "vitest";
import { login, loginV2, getTip } from "./login";
import { phoneLogin } from "xxx/api";
// mock
vi.mock("cxr", () => {
return {
// phoneLogin: vi.fn().mockReturnValue(true),
phoneLogin: vi.fn(()=> true)
};
});
describe("login", () => {
it("should called login function from cxr ", async () => {
login("phone", "jiubugaosuni");
expect(cxrLogin).toBeCalled();
// expect(cxrLogin).toBeCalledWith("phone", "jiubugaosuni");
// expect(cxrLogin).toBeCalledTimes(1);
});
it("v2", () => {
loginV2("phone", "jiubugaosuni");
expect(phoneLogin).toBeCalled();
expect(getTip()).toBe("登录成功拉");
});
});
这里使用vi.mock
去对接口重写,并且使用toBeCalled
、toBeCalledWith
、toBeCalledTimes
等来验证该函数是否调用,调用参数是什么,一共调用了几次。
十五、不知道验证什么-完美主义、功能的目的、小步走-TDD思想
TDD(Test-Driven Development)是一种软件开发方法论,它强调在编写代码之前编写测试用例。其核心思想是通过编写测试用例来驱动代码的开发,以确保代码的正确性和可靠性。
我们只需要记住我们的目标是什么,然后小步走的思想,一个一个去完成,不要一开始就想把所有东西都弄好,这是不现实的。TDD的思想可以应用在我们生活之间
比如下面有一个函数,验证是否是http地址,我们需要如何测试呢
// url http
// url https
// url ""
// url "dslkfj"
// url "123"
// url
export function isHttp(url: string): boolean {
const pattern = /^http:\/\/www\./;
return pattern.test(url);
}
我们只需要编写最小化的代码,因为不可能完美的去写出每个过程,所以,尽可能的写出我们想到的部分,其他的测试出来的问题,我们再去加就好了,不要被完美主义作祟。
import { describe, it, expect } from "vitest";
import { isHttp } from "./utils";
describe("isHttp", () => {
it("should return true for the specific case: http://www.baidu.com", () => {
const url = "http://www.baidu.com";
expect(isHttp(url)).toBeTruthy();
});
it("should return false for non-http URLs", () => {
const url = "https://www.google.com";
expect(isHttp(url)).toBeFalsy();
});
it("should return false for non-http URLs", () => {
const url = "";
expect(isHttp(url)).toBeFalsy();
});
});
十六、可预测性-随机数-日期date
随机数
这里如何对随机数进行验证呢?因为随机数一直是变化的
/**
* 基于 Math.random 生成一个随机字符串
* @param length 字符串长度
* @returns 生成的随机字符串
*/
export function generateRandomString(length: number): string {
let result = "";
const characters = "abcdefghijklmnopqrstuvwxyz";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length); // 生成 0 到字符串长度之间的随机整数
result += characters.charAt(randomIndex); // 将指定位置上的字符添加到结果字符串中
}
return result;
}
验证
通过vi.spyOn
去模拟Math.random
方法并且固定返回它的值为0.1和0.2
import { vi, it, expect, describe } from "vitest";
import { generateRandomString } from "./random";
describe("Math.random", () => {
it("should generate random string", () => {
// vi.spyOn(Math, "random").mockImplementation(() => {
// return 0.1;
// });
vi.spyOn(Math, "random").mockImplementationOnce(() => {
return 0.1;
});
vi.spyOn(Math, "random").mockImplementationOnce(() => {
return 0.2;
});
const result = generateRandomString(2);
expect(result).toBe("fc");
});
});
日期函数
这里如何对日期进行验证呢?因为日期一直是变化的
/**
* 检测今天是否为周五
* @returns 如果今天是周五返回 "开心",否则返回 "不开心"
*/
export function checkFriday(): string {
const today = new Date();
console.log(today.getDay());
if (today.getDay() === 5) {
return "happy";
} else {
return "sad";
}
}
验证
这里使用的是模拟定时器,使用vi.useFakeTimers()
,注意:这里在测试结束后需要使用vi.useRealTimers();
移除掉,然后我们使用vi.setSystemTime(new Date(2023, 3, 21));
去设置系统时间
import { beforeEach, afterEach, vi, it, expect, describe } from "vitest";
import { checkFriday } from "./date";
describe("date", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should be happy when it's Friday", () => {
vi.setSystemTime(new Date(2023, 3, 21));
const result = checkFriday();
expect(result).toBe("happy");
});
it("should be sad when it's not Friday", () => {
vi.setSystemTime(new Date(2023, 3, 22));
const result = checkFriday();
expect(result).toBe("sad");
});
it("third", () => {
checkFriday();
});
});
十七、快速反馈-处理异步代码time、promise
定时器与延时器
export function sayHi() {
setTimeout(() => {
setInterval(() => {
console.log("hi");
}, 100);
}, 1000);
}
export class User {
id: string;
constructor(id: string) {
this.id = id;
}
fetchData(callback: (data: string) => void, delay: number): void {
setTimeout(() => {
const data = `Data for user with id: ${this.id}`;
callback(data);
}, delay);
}
fetchDataV2(callback: (data: string) => void): void {
setTimeout(() => {
const data = `Data for user with id: ${this.id}`;
callback(data);
}, 2000);
}
}
验证
这里我们也是使用vi.useFakeTimers();
去模拟定时器,这里我们有两种方式去进行验证,第一种就是使用vi.advanceTimersToNextTimer()
,vi.advanceTimersByTime(1100);
这个是设置定时器的时间,第二种就是使用vi.spyOn(console, "log");
去模拟打印方法,这种是比较推荐的
import { vi, it, expect, describe } from "vitest";
import { sayHi } from "./setInterval";
describe("setInterval", () => {
it("should call one", () => {
vi.useFakeTimers();
vi.spyOn(console, "log");
sayHi();
// vi.advanceTimersToNextTimer()
// vi.advanceTimersToNextTimer()
// vi.advanceTimersByTime(1100);
expect(console.log).toBeCalledWith("hi");
});
});
import { vi, it, expect, describe } from "vitest";
import { User } from "./setTimeout";
describe("setTimeout", () => {
it("should fetch User data", () => {
vi.useFakeTimers();
const user = new User("1");
const callback = vi.fn();
user.fetchDataV2(callback);
// vi.advanceTimersByTime(1000)
// vi.advanceTimersToNextTimer();
const userA = new User("1");
const callbackA = vi.fn();
userA.fetchDataV2(callbackA);
// vi.advanceTimersToNextTimer();
vi.runAllTimers();
expect(callback).toBeCalledWith("Data for user with id: 1");
expect(callbackA).toBeCalledWith("Data for user with id: 1");
});
});
promise
export class View {
count: number = 1;
render() {
Promise.resolve()
.then(() => {
this.count = 2;
})
.then(() => {
this.count = 3;
});
}
}
export function fetchUserData() {
return new Promise((resolve, reject) => {
resolve("1");
});
}
export function delay(time: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("ok");
}, time);
});
}
验证
import { vi, it, expect, describe } from "vitest";
import { delay, fetchUserData } from "./index";
describe("Promise", () => {
it("normal", async () => {
const result = await fetchUserData();
expect(result).toBe("1");
});
it("delay", async () => {
vi.useFakeTimers();
// vi.advanceTimersToNextTimer()
// const result = await delay(1000);
const result = delay(100);
vi.advanceTimersToNextTimer();
expect(result).resolves.toBe("ok");
});
});
第二种是结合flush-promises
这个库来使用
import { it, expect, describe } from "vitest";
import { View } from "./view";
import flushPromises from "flush-promises";
describe("View", () => {
it("should change count", async () => {
const view = new View();
view.render();
await flushPromises();
expect(view.count).toBe(3);
});
});
十八、API的多种测试方案
1、直接mock axios
直接上代码,通过直接对axios
进行mock
来定义返回结果---------不推荐使用
import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import axios from "axios";
vi.mock("axios");
test("add todo", async () => {
// 准备数据
vi.mocked(axios.post).mockImplementation((path, { title }: any) => {
return Promise.resolve({
data: { data: { todo: { id: 1, title } }, state: 1 },
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos[0].title).toBe(title);
});
test("should not add todo when title is empty string", async () => {
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("remove todo", async () => {
vi.mocked(axios.post).mockImplementationOnce((path, { title }: any) => {
return Promise.resolve({
data: { data: { todo: { id: 1, title } }, state: 1 },
});
});
vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
return Promise.resolve({
data: { data: { id }, state: 1 },
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const todo = await todoStore.addTodo("吃饭"); // round-trip
// 调用
await todoStore.removeTodo(todo!.id);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("should throw error when removed id does not exist ", async () => {
// 准备数据
vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
return Promise.resolve({
data: { data: null, state: 0 },
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
expect(async () => {
// 调用
await todoStore.removeTodo(2);
// 抛出一个错误
}).rejects.toThrowError("id:2 不存在");
});
test("update todo list", async () => {
const todoList = [{ id: 1, title: "写代码" }];
vi.mocked(axios.get).mockResolvedValue({ data: { data: { todoList } } });
setActivePinia(createPinia());
const todoStore = useTodoStore();
await todoStore.updateTodoList();
expect(todoStore.todos[0].title).toBe("写代码");
});
2、mock中间层
这个的意思指的是,只对行为进行验证,行为产生的结果进行mock
-----------推荐使用
import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { fetchAddTodo, fetchRemoveTodo, fetchTodoList } from "../api";
vi.mock("../api");
// SUT create list
// create list
// add todo to todos , todos' length is 1
test("should add todo to the list when successful", async () => {
// 准备数据
vi.mocked(fetchAddTodo).mockImplementation((title) => {
return Promise.resolve({
data: { todo: { id: 1, title } },
state: 1,
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos[0].title).toBe(title);
});
test("should not be added todo when network is error", async () => {
// 准备数据
vi.mocked(fetchAddTodo).mockImplementation((title) => {
return Promise.reject("network error");
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
expect(async () => {
await todoStore.addTodo(title);
}).rejects.toThrowError("network error");
});
test("should not add a todo when title is empty", async () => {
// 准备数据
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("remove todo when todo is", async () => {
// 准备数据
vi.mocked(fetchAddTodo).mockImplementation((title) => {
return Promise.resolve({
data: { todo: { id: 1, title } },
state: 1,
});
});
vi.mocked(fetchRemoveTodo).mockImplementationOnce((id) => {
return Promise.resolve({
data: { id },
state: 1,
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const todo = await todoStore.addTodo("吃饭"); // round-trip
// 调用
await todoStore.removeTodo(todo!.id);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("should throw a error when removed id does not exist", async () => {
// 准备数据
vi.mocked(fetchRemoveTodo).mockImplementationOnce(() => {
return Promise.resolve({
data: null,
state: 0,
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
// 调用
expect(async () => {
await todoStore.removeTodo(2);
}).rejects.toThrowError("id:2 does not exist");
});
test("update todo list", async () => {
const todoList = [{ id: 1, title: "写代码" }];
vi.mocked(fetchTodoList).mockResolvedValue({ data: { todoList } });
setActivePinia(createPinia());
const todoStore = useTodoStore();
await todoStore.updateTodoList();
expect(todoStore.todos[0].title).toBe("写代码");
});
3、使用mock server worker
这里指的是使用msw
这个库来mock返回结果,相当于利用中间件去测试
缺点:就是需要去学习新的库,不太推荐使用
import { beforeAll, afterEach, afterAll, test, expect} from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { server } from "../mocks/server";
import { mockAddTodo, mockRemoveTodo, mockTodoList } from "../mocks/handlers";
test.todo("sad path")
test("add todo", async () => {
// koa express
server.use(mockAddTodo());
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos[0].title).toBe(title);
});
test("remove todo", async () => {
server.use(mockAddTodo(), mockRemoveTodo());
setActivePinia(createPinia());
const todoStore = useTodoStore();
const todo = await todoStore.addTodo("吃饭"); // round-trip
// 调用
await todoStore.removeTodo(todo!.id);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("update todo list", async () => {
const todoList = [{ id: 1, title: "写代码" }];
server.use(mockTodoList(todoList));
setActivePinia(createPinia());
const todoStore = useTodoStore();
await todoStore.updateTodoList();
expect(todoStore.todos[0].title).toBe("写代码");
});
十九、参数化验证
提供在多个测试case中复用相同的测试逻辑的方法
比如我们想去验证一个正则表达式
export function emailValidator(email: string): boolean {
const regex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/;
return regex.test(email);
}
可能我们的测试case非常多,可以看到非常多重复的case
import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"
describe("emailValidator", () => {
it("should return true for valid email", () => {
const email = "valid-email@example.com"
expect(emailValidator(email)).toBe(true)
})
it("should return false for invalid email without domain extension", () => {
const email = "invalid.email@example"
expect(emailValidator(email)).toBe(false)
})
it("should return false for invalid email with extra dot at the end", () => {
const email = "another.invalid.email@example."
expect(emailValidator(email)).toBe(false)
})
it("should return false for invalid email with missing '@'", () => {
const email = "yet.another.invalid.email.example.com"
expect(emailValidator(email)).toBe(false)
})
})
解决办法:我们可以利用工具提供的方法
import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"
describe("emailValidator", () => {
it.each([
["valid-email@example.com", true],
["invalid.email@example", false],
["another.invalid.email@example.", false],
["yet.another.invalid.email.example.com", false],
])("should return %s when email is %s", (email, expected) => {
expect(emailValidator(email)).toBe(expected)
})
it.each([{ email: "valid-email@example.com", expected: true }])(
"should return $email when email is $expected",
({ email, expected }) => {
console.log(email, expected)
expect(emailValidator(email)).toBe(expected)
}
)
it.each`
email | expected
${"valid-email@example.com"} | ${true}
${"invalid.email@example"} | ${false}
`("should return $email when email is $expected", ({ email, expected }) => {
console.log(email, expected)
expect(emailValidator(email)).toBe(expected)
})
it.each`
email | expected
${{ a: "aaaaa" }} | ${true}
${[]} | ${true}
${false} | ${true}
`("should return $email.a when email is $expected", ({ email, expected }) => {
console.log(email, expected)
expect(false).toBe(true)
// expect(emailValidator(email)).toBe(expected);
})
})
我们使用it.each
,并且使用模版字符串的语法,在参数中,我们还可以使用$email.a
去获取参数,其中第三种和第四种方法是最佳实践
第一种方法因为在执行过程中,不太好查看错误信息,所以不推荐使用—
theme: z-blue
highlight: atom-one-light
# 2024年,重新探索前端单元测试之路,从入门到精通01-Vitest
# 2024年,重新探索前端单元测试之路,从入门到精通02-Vitest
十四、行为验证
这里主要讲的是对于函数的调用次数,以及函数参数等验证
这里举得例子是登录接口
import { phoneLogin } from "xxx/api";
const state = {
tipString: "",
};
export function login(username: string, password: string) {
phoneLogin(username, password);
}
export function loginV2(username: string, password: string) {
const isLogin = phoneLogin(username, password);
if (isLogin) {
state.tipString = "登录成功拉";
}
}
export function getTip() {
return state.tipString;
}
接下来看看我们是怎么测试这个登录接口的
import { vi, it, expect, describe } from "vitest";
import { login, loginV2, getTip } from "./login";
import { phoneLogin } from "xxx/api";
// mock
vi.mock("cxr", () => {
return {
// phoneLogin: vi.fn().mockReturnValue(true),
phoneLogin: vi.fn(()=> true)
};
});
describe("login", () => {
it("should called login function from cxr ", async () => {
login("phone", "jiubugaosuni");
expect(cxrLogin).toBeCalled();
// expect(cxrLogin).toBeCalledWith("phone", "jiubugaosuni");
// expect(cxrLogin).toBeCalledTimes(1);
});
it("v2", () => {
loginV2("phone", "jiubugaosuni");
expect(phoneLogin).toBeCalled();
expect(getTip()).toBe("登录成功拉");
});
});
这里使用vi.mock
去对接口重写,并且使用toBeCalled
、toBeCalledWith
、toBeCalledTimes
等来验证该函数是否调用,调用参数是什么,一共调用了几次。
十五、不知道验证什么-完美主义、功能的目的、小步走-TDD思想
TDD(Test-Driven Development)是一种软件开发方法论,它强调在编写代码之前编写测试用例。其核心思想是通过编写测试用例来驱动代码的开发,以确保代码的正确性和可靠性。
我们只需要记住我们的目标是什么,然后小步走的思想,一个一个去完成,不要一开始就想把所有东西都弄好,这是不现实的。TDD的思想可以应用在我们生活之间
比如下面有一个函数,验证是否是http地址,我们需要如何测试呢
// url http
// url https
// url ""
// url "dslkfj"
// url "123"
// url
export function isHttp(url: string): boolean {
const pattern = /^http:\/\/www\./;
return pattern.test(url);
}
我们只需要编写最小化的代码,因为不可能完美的去写出每个过程,所以,尽可能的写出我们想到的部分,其他的测试出来的问题,我们再去加就好了,不要被完美主义作祟。
import { describe, it, expect } from "vitest";
import { isHttp } from "./utils";
describe("isHttp", () => {
it("should return true for the specific case: http://www.baidu.com", () => {
const url = "http://www.baidu.com";
expect(isHttp(url)).toBeTruthy();
});
it("should return false for non-http URLs", () => {
const url = "https://www.google.com";
expect(isHttp(url)).toBeFalsy();
});
it("should return false for non-http URLs", () => {
const url = "";
expect(isHttp(url)).toBeFalsy();
});
});
十六、可预测性-随机数-日期date
随机数
这里如何对随机数进行验证呢?因为随机数一直是变化的
/**
* 基于 Math.random 生成一个随机字符串
* @param length 字符串长度
* @returns 生成的随机字符串
*/
export function generateRandomString(length: number): string {
let result = "";
const characters = "abcdefghijklmnopqrstuvwxyz";
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length); // 生成 0 到字符串长度之间的随机整数
result += characters.charAt(randomIndex); // 将指定位置上的字符添加到结果字符串中
}
return result;
}
验证
通过vi.spyOn
去模拟Math.random
方法并且固定返回它的值为0.1和0.2
import { vi, it, expect, describe } from "vitest";
import { generateRandomString } from "./random";
describe("Math.random", () => {
it("should generate random string", () => {
// vi.spyOn(Math, "random").mockImplementation(() => {
// return 0.1;
// });
vi.spyOn(Math, "random").mockImplementationOnce(() => {
return 0.1;
});
vi.spyOn(Math, "random").mockImplementationOnce(() => {
return 0.2;
});
const result = generateRandomString(2);
expect(result).toBe("fc");
});
});
日期函数
这里如何对日期进行验证呢?因为日期一直是变化的
/**
* 检测今天是否为周五
* @returns 如果今天是周五返回 "开心",否则返回 "不开心"
*/
export function checkFriday(): string {
const today = new Date();
console.log(today.getDay());
if (today.getDay() === 5) {
return "happy";
} else {
return "sad";
}
}
验证
这里使用的是模拟定时器,使用vi.useFakeTimers()
,注意:这里在测试结束后需要使用vi.useRealTimers();
移除掉,然后我们使用vi.setSystemTime(new Date(2023, 3, 21));
去设置系统时间
import { beforeEach, afterEach, vi, it, expect, describe } from "vitest";
import { checkFriday } from "./date";
describe("date", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should be happy when it's Friday", () => {
vi.setSystemTime(new Date(2023, 3, 21));
const result = checkFriday();
expect(result).toBe("happy");
});
it("should be sad when it's not Friday", () => {
vi.setSystemTime(new Date(2023, 3, 22));
const result = checkFriday();
expect(result).toBe("sad");
});
it("third", () => {
checkFriday();
});
});
十七、快速反馈-处理异步代码time、promise
定时器与延时器
export function sayHi() {
setTimeout(() => {
setInterval(() => {
console.log("hi");
}, 100);
}, 1000);
}
export class User {
id: string;
constructor(id: string) {
this.id = id;
}
fetchData(callback: (data: string) => void, delay: number): void {
setTimeout(() => {
const data = `Data for user with id: ${this.id}`;
callback(data);
}, delay);
}
fetchDataV2(callback: (data: string) => void): void {
setTimeout(() => {
const data = `Data for user with id: ${this.id}`;
callback(data);
}, 2000);
}
}
验证
这里我们也是使用vi.useFakeTimers();
去模拟定时器,这里我们有两种方式去进行验证,第一种就是使用vi.advanceTimersToNextTimer()
,vi.advanceTimersByTime(1100);
这个是设置定时器的时间,第二种就是使用vi.spyOn(console, "log");
去模拟打印方法,这种是比较推荐的
import { vi, it, expect, describe } from "vitest";
import { sayHi } from "./setInterval";
describe("setInterval", () => {
it("should call one", () => {
vi.useFakeTimers();
vi.spyOn(console, "log");
sayHi();
// vi.advanceTimersToNextTimer()
// vi.advanceTimersToNextTimer()
// vi.advanceTimersByTime(1100);
expect(console.log).toBeCalledWith("hi");
});
});
import { vi, it, expect, describe } from "vitest";
import { User } from "./setTimeout";
describe("setTimeout", () => {
it("should fetch User data", () => {
vi.useFakeTimers();
const user = new User("1");
const callback = vi.fn();
user.fetchDataV2(callback);
// vi.advanceTimersByTime(1000)
// vi.advanceTimersToNextTimer();
const userA = new User("1");
const callbackA = vi.fn();
userA.fetchDataV2(callbackA);
// vi.advanceTimersToNextTimer();
vi.runAllTimers();
expect(callback).toBeCalledWith("Data for user with id: 1");
expect(callbackA).toBeCalledWith("Data for user with id: 1");
});
});
promise
export class View {
count: number = 1;
render() {
Promise.resolve()
.then(() => {
this.count = 2;
})
.then(() => {
this.count = 3;
});
}
}
export function fetchUserData() {
return new Promise((resolve, reject) => {
resolve("1");
});
}
export function delay(time: number) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("ok");
}, time);
});
}
验证
import { vi, it, expect, describe } from "vitest";
import { delay, fetchUserData } from "./index";
describe("Promise", () => {
it("normal", async () => {
const result = await fetchUserData();
expect(result).toBe("1");
});
it("delay", async () => {
vi.useFakeTimers();
// vi.advanceTimersToNextTimer()
// const result = await delay(1000);
const result = delay(100);
vi.advanceTimersToNextTimer();
expect(result).resolves.toBe("ok");
});
});
第二种是结合flush-promises
这个库来使用
import { it, expect, describe } from "vitest";
import { View } from "./view";
import flushPromises from "flush-promises";
describe("View", () => {
it("should change count", async () => {
const view = new View();
view.render();
await flushPromises();
expect(view.count).toBe(3);
});
});
十八、API的多种测试方案
1、直接mock axios
直接上代码,通过直接对axios
进行mock
来定义返回结果---------不推荐使用
import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import axios from "axios";
vi.mock("axios");
test("add todo", async () => {
// 准备数据
vi.mocked(axios.post).mockImplementation((path, { title }: any) => {
return Promise.resolve({
data: { data: { todo: { id: 1, title } }, state: 1 },
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos[0].title).toBe(title);
});
test("should not add todo when title is empty string", async () => {
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("remove todo", async () => {
vi.mocked(axios.post).mockImplementationOnce((path, { title }: any) => {
return Promise.resolve({
data: { data: { todo: { id: 1, title } }, state: 1 },
});
});
vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
return Promise.resolve({
data: { data: { id }, state: 1 },
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const todo = await todoStore.addTodo("吃饭"); // round-trip
// 调用
await todoStore.removeTodo(todo!.id);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("should throw error when removed id does not exist ", async () => {
// 准备数据
vi.mocked(axios.post).mockImplementationOnce((path, { id }: any) => {
return Promise.resolve({
data: { data: null, state: 0 },
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
expect(async () => {
// 调用
await todoStore.removeTodo(2);
// 抛出一个错误
}).rejects.toThrowError("id:2 不存在");
});
test("update todo list", async () => {
const todoList = [{ id: 1, title: "写代码" }];
vi.mocked(axios.get).mockResolvedValue({ data: { data: { todoList } } });
setActivePinia(createPinia());
const todoStore = useTodoStore();
await todoStore.updateTodoList();
expect(todoStore.todos[0].title).toBe("写代码");
});
2、mock中间层
这个的意思指的是,只对行为进行验证,行为产生的结果进行mock
-----------推荐使用
import { test, expect, vi } from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { fetchAddTodo, fetchRemoveTodo, fetchTodoList } from "../api";
vi.mock("../api");
// SUT create list
// create list
// add todo to todos , todos' length is 1
test("should add todo to the list when successful", async () => {
// 准备数据
vi.mocked(fetchAddTodo).mockImplementation((title) => {
return Promise.resolve({
data: { todo: { id: 1, title } },
state: 1,
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos[0].title).toBe(title);
});
test("should not be added todo when network is error", async () => {
// 准备数据
vi.mocked(fetchAddTodo).mockImplementation((title) => {
return Promise.reject("network error");
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
expect(async () => {
await todoStore.addTodo(title);
}).rejects.toThrowError("network error");
});
test("should not add a todo when title is empty", async () => {
// 准备数据
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("remove todo when todo is", async () => {
// 准备数据
vi.mocked(fetchAddTodo).mockImplementation((title) => {
return Promise.resolve({
data: { todo: { id: 1, title } },
state: 1,
});
});
vi.mocked(fetchRemoveTodo).mockImplementationOnce((id) => {
return Promise.resolve({
data: { id },
state: 1,
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
const todo = await todoStore.addTodo("吃饭"); // round-trip
// 调用
await todoStore.removeTodo(todo!.id);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("should throw a error when removed id does not exist", async () => {
// 准备数据
vi.mocked(fetchRemoveTodo).mockImplementationOnce(() => {
return Promise.resolve({
data: null,
state: 0,
});
});
setActivePinia(createPinia());
const todoStore = useTodoStore();
// 调用
expect(async () => {
await todoStore.removeTodo(2);
}).rejects.toThrowError("id:2 does not exist");
});
test("update todo list", async () => {
const todoList = [{ id: 1, title: "写代码" }];
vi.mocked(fetchTodoList).mockResolvedValue({ data: { todoList } });
setActivePinia(createPinia());
const todoStore = useTodoStore();
await todoStore.updateTodoList();
expect(todoStore.todos[0].title).toBe("写代码");
});
3、使用mock server worker
这里指的是使用msw
这个库来mock返回结果,相当于利用中间件去测试
缺点:就是需要去学习新的库,不太推荐使用
import { beforeAll, afterEach, afterAll, test, expect} from "vitest";
import { useTodoStore } from "./todo";
import { setActivePinia, createPinia } from "pinia";
import { server } from "../mocks/server";
import { mockAddTodo, mockRemoveTodo, mockTodoList } from "../mocks/handlers";
test.todo("sad path")
test("add todo", async () => {
// koa express
server.use(mockAddTodo());
setActivePinia(createPinia());
const todoStore = useTodoStore();
const title = "吃饭";
// 调用
await todoStore.addTodo(title);
// 验证
expect(todoStore.todos[0].title).toBe(title);
});
test("remove todo", async () => {
server.use(mockAddTodo(), mockRemoveTodo());
setActivePinia(createPinia());
const todoStore = useTodoStore();
const todo = await todoStore.addTodo("吃饭"); // round-trip
// 调用
await todoStore.removeTodo(todo!.id);
// 验证
expect(todoStore.todos.length).toBe(0);
});
test("update todo list", async () => {
const todoList = [{ id: 1, title: "写代码" }];
server.use(mockTodoList(todoList));
setActivePinia(createPinia());
const todoStore = useTodoStore();
await todoStore.updateTodoList();
expect(todoStore.todos[0].title).toBe("写代码");
});
十九、参数化验证
提供在多个测试case中复用相同的测试逻辑的方法
比如我们想去验证一个正则表达式
export function emailValidator(email: string): boolean {
const regex = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/;
return regex.test(email);
}
可能我们的测试case非常多,可以看到非常多重复的case
import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"
describe("emailValidator", () => {
it("should return true for valid email", () => {
const email = "valid-email@example.com"
expect(emailValidator(email)).toBe(true)
})
it("should return false for invalid email without domain extension", () => {
const email = "invalid.email@example"
expect(emailValidator(email)).toBe(false)
})
it("should return false for invalid email with extra dot at the end", () => {
const email = "another.invalid.email@example."
expect(emailValidator(email)).toBe(false)
})
it("should return false for invalid email with missing '@'", () => {
const email = "yet.another.invalid.email.example.com"
expect(emailValidator(email)).toBe(false)
})
})
解决办法:我们可以利用工具提供的方法
import { emailValidator } from "./emailValidator"
import { it, expect, describe } from "vitest"
describe("emailValidator", () => {
it.each([
["valid-email@example.com", true],
["invalid.email@example", false],
["another.invalid.email@example.", false],
["yet.another.invalid.email.example.com", false],
])("should return %s when email is %s", (email, expected) => {
expect(emailValidator(email)).toBe(expected)
})
it.each([{ email: "valid-email@example.com", expected: true }])(
"should return $email when email is $expected",
({ email, expected }) => {
console.log(email, expected)
expect(emailValidator(email)).toBe(expected)
}
)
it.each`
email | expected
${"valid-email@example.com"} | ${true}
${"invalid.email@example"} | ${false}
`("should return $email when email is $expected", ({ email, expected }) => {
console.log(email, expected)
expect(emailValidator(email)).toBe(expected)
})
it.each`
email | expected
${{ a: "aaaaa" }} | ${true}
${[]} | ${true}
${false} | ${true}
`("should return $email.a when email is $expected", ({ email, expected }) => {
console.log(email, expected)
expect(false).toBe(true)
// expect(emailValidator(email)).toBe(expected);
})
})
我们使用it.each
,并且使用模版字符串的语法,在参数中,我们还可以使用$email.a
去获取参数,其中第三种和第四种方法是最佳实践
第一种方法因为在执行过程中,不太好查看错误信息,所以不推荐使用