# 2024年,重新探索前端单元测试之路,从入门到精通01-Vitest
十一、实现一个自己的 mini-test-runner
1、目标
- 通过自己实现一个
mini-test-runner
来理解测试框架的api
使用
2、实现
- 首先模拟
vitest
的方法,例如test/it
,describe
,expect
,生命周期函数等
js
// 1.spec.js
import { test, run,describe } from "./core.js"
describe("", () => {
test("first test case", () => {
console.log("1. first test case")
})
test.only("second test case", () => {
console.log("2. second test case")
})
})
run()
js
// core.js
let testCallbacks = []
let onlyCallbacks = []
let describeCallbacks = []
let beforeAllCallbacks = []
let beforeEachCallbacks = []
let afterEachCallbacks = []
let afterAllCallbacks = []
export function describe(name, callback) {
describeCallbacks.push({ name, callback })
callback()
}
export function test(name, callback) {
testCallbacks.push({ name, callback })
}
test.only = function (name, callback) {
onlyCallbacks.push({ name, callback })
}
export const it = test
export function expect(pre) {
return {
toBe: cur => {
if (pre === cur) {
console.log("通过")
} else {
throw new Error(`不通过,pre: ${pre} 不等于 cur: ${cur}`)
}
},
toEqual: cur => {
if (typeof cur === "object") {
throw new Error(`不通过,类型只能为object`)
}
if (pre === cur) {
console.log("通过")
} else {
throw new Error(`不通过,pre: ${pre} 不等于 cur: ${cur}`)
}
},
}
}
export function beforeAll(fn){
beforeAllCallbacks.push(fn)
}
export function beforeEach(fn){
beforeEachCallbacks.push(fn)
}
export function AfterEach(fn){
afterEachCallbacks.push(fn)
}
export function AfterAll(fn){
afterAllCallbacks.push(fn)
}
export function run() {
const tests = onlyCallbacks.length > 0 ? onlyCallbacks : testCallbacks
for (const { name, callback } of tests) {
try {
callback()
console.log(`ok: ${name}`)
} catch (error) {
console.log(`error: ${name}`)
}
}
}
主要的执行逻辑在run
方法,通过收集test
等内部的回调去执行,并且在执行test.only
的时候需要去判断一下
js
const tests = onlyCallbacks.length > 0 ? onlyCallbacks : testCallbacks
这样就可以只执行only
的回调了!!!
接下来我们继续补充生命周期的执行方法,其实就是改造run
方法
js
export function run() {
// 执行总的beforeAll回调
beforeAllCallbacks.forEach(fn => fn())
const tests = onlyCallbacks.length > 0 ? onlyCallbacks : testCallbacks
for (const { name, callback } of tests) {
// 执行beforeEach的回调
beforeEachCallbacks.forEach(fn => fn())
try {
callback()
console.log(`ok: ${name}`)
} catch (error) {
console.log(`error: ${name}`)
}
// 执行afterEach的回调
afterEachCallbacks.forEach(fn => fn())
}
// 执行总的afterAll回调
afterAllCallbacks.forEach(fn => fn())
}
如此一来,我们对于基本的vitest
的api
的功能就已经完成,接下来我们继续完善,我们写一个插件,让它能够自动运行测试代码,而不是我们手动调用run
方法
3、实现自动运行测试代码
js
import fs from "fs/promises"
import { glob } from "glob" // 一个匹配文件名的插件
// 查找所有以 `.spec.js` 结尾的测试文件
const testFiles = glob.sync("**/*.spec.js", { ignore: "node_modules/**" })
console.log("testFiles", testFiles)
运行代码返回结果如下
使用fs
模块对文件进行处理
js
import fs from "fs/promises"
import { glob } from "glob"
// 查找所有以 `.spec.js` 结尾的测试文件
const testFiles = glob.sync("**/*.spec.js", { ignore: "node_modules/**" })
// 运行所有测试文件
for (const testFile of testFiles) {
const fileContent = await fs.readFile(testFile, "utf-8")
console.log('',fileContent);
}
运行代码返回结果如下
接下来我们需要自动去运行代码,我们把run
方法去掉,得需要程序自动去运行
并且我们不只有一个测试文件,所以需要对所有测试文件的内容进行打包合并到一个文件内去运行,这里我使用edbulid
js
import fs from "fs/promises"
import { glob } from "glob"
import { build } from "esbuild"
// 查找所有以 `.spec.js` 结尾的测试文件
const testFiles = glob.sync("**/*.spec.js", { ignore: "node_modules/**" })
// 运行所有测试文件
for (const testFile of testFiles) {
const fileContent = await fs.readFile(testFile, "utf-8")
await runModule(fileContent)
}
async function runModule(fileContent) {
try {
const result = await build({
stdin: {
contents: fileContent,
resolveDir: process.cwd(),
},
write: false,
bundle: true,
target: "esnext",
})
const transformedCode = result.outputFiles[0].text
console.log("result", transformedCode)
} catch (error) {
console.error("Error executing module:", error)
}
}
运行后看见确实合并在一起了
4、最终代码
js
import fs from "fs/promises"
import { glob } from "glob"
import { build } from "esbuild"
// 查找所有以 `.spec.js` 结尾的测试文件
const testFiles = glob.sync("**/*.spec.js", { ignore: "node_modules/**" })
// 运行所有测试文件
for (const testFile of testFiles) {
const fileContent = await fs.readFile(testFile, "utf-8")
await runModule(fileContent + "import { run } from './core.js'; run()")
}
async function runModule(fileContent) {
try {
// 转换代码为 CommonJS 格式并捆绑依赖
const result = await build({
stdin: {
contents: fileContent,
resolveDir: process.cwd(),
},
write: false,
bundle: true,
target: "esnext",
})
// 获取转换后的代码
const transformedCode = result.outputFiles[0].text
// 执行转换后的代码
const runCode = new Function(transformedCode)
runCode()
} catch (error) {
console.error("Error executing module:", error)
}
}
最后我们使用暴力的方式,去运行测试代码
所有文件都测试完毕
以下内容等待更新...