vitest
基础
安装
pnpm add -D vitest
工作区
工作区配置文件
- vitest.workspace.ts;
import { defineWorkspace } from "vitest/config";
export default defineWorkspace([
'packages/*/vitest.config.{e2e,unit}.ts'
{
test: {
name: "happy-dom",
root: "./shared_tests",
environment: "happy-dom",
setupFiles: ["./setup.happy-dom.ts"],
},
},
{
test: {
name: "node",
root: "./shared_tests",
environment: "node",
setupFiles: ["./setup.node.ts"],
},
},
]);
项目配置文件
- 同 vitest.config.ts;
- 工作区项目不支持所有配置属性;
- 使用 defineProject 方法;
import { defineProject } from "vitest/config";
export default defineProject({
test: {
environment: "jsdom",
// "reporters" is not supported in a project config,
// so it will show an error
reporters: ["json"],
},
});
运行测试
# 所有项目
pnpm run test
# 指定项目
pnpm run test --project e2e --project unit
命令行
基础命令
# 启动测试, 开发模式进入 watch 模式, ci 进入 run 模式
# 使用正则表达式匹配文件
vitest foobar
# 单次运行
vitest run
# 监听文件变化, 变化一次运行一次
vitest watch
# 运行基准测试
vitest bench
命令行选项
覆盖测试
运行覆盖测试
- 使用
--coverage
标识;
vitest run --coverage
配置覆盖测试
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
reporter: ["text", "json", "html"],
// ...
},
},
});
快照测试
运行快照测试
- 使用
toMatchSnapshot()
API; - 第一次执行快照测试,保存快照至指定位置;
import { expect, it } from "vitest";
it("toUpperCase", () => {
const result = toUpperCase("foobar");
expect(result).toMatchSnapshot();
});
内联快照
- 使用
toMatchInlineSnapshot()
API; - 将快照文件存储在测试文件中;
import { expect, it } from "vitest";
it("toUpperCase", () => {
const result = toUpperCase("foobar");
expect(result).toMatchInlineSnapshot();
});
更新快照
- 使用
--update
标识;
vitest --update
Mocking
- 模拟测试中某模块结果;
- 我一般不用,略;
报告器
- 不同格式显示测试结果;
import { defineConfig } from "vite";
export default defineConfig({
test: {
reporters: ["verbose"],
},
});
测试 API
test
基础
- 定义一次测试;
import { expect, test } from "vitest";
test("should work as expected", () => {
expect(Math.sqrt(4)).toBe(2);
});
跳过测试
- 跳过测试但不删除;
import { assert, test } from "vitest";
test.skip("skipped test", () => {
// Test skipped, no error
assert.equal(Math.sqrt(4), 3);
});
bench
基础
- 性能基准测试;
import { bench } from "vitest";
bench(
"normal sorting",
() => {
const x = [1, 5, 4, 2, 3];
x.sort((a, b) => {
return a - b;
});
},
{ time: 1000 }
);
跳过测试
- 跳过测试但不删除;
import { bench } from "vitest";
bench.skip("normal sorting", () => {
const x = [1, 5, 4, 2, 3];
x.sort((a, b) => {
return a - b;
});
});
describe
基础
- test 或 bench 的抽象;
import { expect, test, describe } from "vitest";
import { sum } from "./sum";
describe("Test the Route class", () => {
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
});
跳过测试
- 跳过测试但不删除;
import { expect, test, describe } from "vitest";
import { sum } from "./sum";
describe.skip("Test the Route class", () => {
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
});
预先设置
重复设置
// 每次执行前运行
beforeEach(() => {
initializeCityDatabase();
});
// 每次执行后运行
afterEach(() => {
clearCityDatabase();
});
一次性设置
// 所有测试执行前运行一次
beforeAll(() => {
return initializeCityDatabase();
});
// 所有测试执行后运行一次
afterAll(() => {
return clearCityDatabase();
});
作用域
// 应用于所有测试
beforeEach(() => {
return initializeCityDatabase();
});
describe("matching cities to foods", () => {
// 只应用于该 describe
beforeEach(() => {
return initializeFoodDatabase();
});
});
断言
软断言
- 断言失败是不会终止测试;
- 而是标记该测试为失败,继续进行其余测试;
import { expect, test } from "vitest";
test("expect.soft test", () => {
expect.soft(1 + 1).toBe(3); // mark the test as fail and continue
expect.soft(1 + 2).toBe(4); // mark the test as fail and continue
});
取反
import { expect, test } from "vitest";
const input = Math.sqrt(16);
expect(input).not.to.equal(2); // chai API
expect(input).not.toBe(2); // jest API
相等
// 比较原子类型或引用是否相等
test("two plus two is four", () => {
expect(2 + 2).toBe(4);
});
// 比较浮点数类型
test("decimals are rounded to 5 after the point", () => {
// 0.2 + 0.1 is 0.30000 | "000000000004" removed
expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
// nothing from 0.30000000000000004 is removed
expect(0.2 + 0.1).not.toBeCloseTo(0.3, 50);
});
// 比较对象结构是否相等
// 忽略 undefined 属性
test("object assignment", () => {
const data = { one: 1 };
data["two"] = 2;
expect(data).toEqual({ one: 1, two: 2 });
});
// 严格比较对象结构是否相等
// 检查 undefined 属性, 数组稀疏性, 对象类型
test("object assignment", () => {
const data = { one: 1 };
data["two"] = 2;
expect(data).toStrictEqual({ one: 1, two: 2 });
});
布尔判断
class A {}
test("null", () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).toBeUndefined();
expect(n).toBeTruthy();
expect(n).toBeFalsy();
expect(n).toBeNaN();
expect(n).toBeTypeOf();
expect(new A()).toBeInstanceOf(A);
});
数字
test("two plus two", () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
});
数组
const shoppingList = [
"diapers",
"kleenex",
"trash bags",
"paper towels",
"milk",
];
test("the shopping list has milk on it", () => {
expect(shoppingList).toContain("milk");
expect(new Set(shoppingList)).toContain("milk");
// toContain 的 Equal 版本
expect(new Set(shoppingList)).toContainEqual("milk");
});
属性
import { expect, test } from "vitest";
test("toHaveLength", () => {
// 是否具有 .length 属性
expect([1, 2, 3]).toHaveLength(3);
// 是否具有 isActive 属性
expect(invoice).toHaveProperty("isActive");
});
正则表达式
import { expect, test } from "vitest";
test("there is no I in team", () => {
expect("team").not.toMatch(/I/);
});
异常
import { expect, test } from "vitest";
function compileAndroidCode() {
throw new Error("you are using the wrong JDK!");
}
// 使用箭头函数包裹
test("compiling android goes as expected", () => {
expect(() => compileAndroidCode()).toThrowError();
expect(() => compileAndroidCode()).toThrowError(Error);
});
快照
- [[#快照测试]];
异步
import { expect, test } from "vitest";
test("the data is peanut butter", () => {
return expect(fetchData()).resolves.toBe("peanut butter");
});
test("the fetch fails with an error", () => {
return expect(fetchData()).rejects.toMatch("error");
});
测试环境
常见测试环境
- node:默认环境;
- jsdom:使用 jsdom 模拟浏览器环境;
- happy-dom:使用 happy-dom 模拟浏览器环境;
- edge-runtime:模拟 vercel edge 环境;
配置测试环境
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineProject({
test: {
environment: "jsdom",
},
});
调试
vitest
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}
配置
配置文件
- vitest.config.ts;
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
// ...
},
});
配置选项
- 配置选项;
import path from "path";
import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
test: {
alias: {
"@": path.resolve(process.cwd(), "./src"),
},
include: ["**/src/**/*.test.ts"],
coverage: {
include: ["**/src/**"],
exclude: [
...configDefaults.coverage.exclude!,
"**/type/**",
"**/index.ts",
],
},
},
});