2024年,重新探索前端单元测试之路,从入门到精通02-Vitest

# 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())
}

如此一来,我们对于基本的vitestapi的功能就已经完成,接下来我们继续完善,我们写一个插件,让它能够自动运行测试代码,而不是我们手动调用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)
  }
}
​

最后我们使用暴力的方式,去运行测试代码

所有文件都测试完毕

以下内容等待更新...

相关推荐
洞窝技术1 天前
增量代码自动Review工具:洞窝在AI上的探索和实践
jenkins·ai编程·测试
littleplayer1 天前
iOS 单元测试与 UI 测试详解-DeepSeek
前端·单元测试·测试
陈哥聊测试2 天前
开发认为测试不及时,测试吐槽工作量太大?
后端·测试·devops
咖啡配辣条2 天前
MySQL数据库教程03
数据库·测试
郝同学的测开笔记3 天前
SQL查询深度解析:获取上周完课机构统计
mysql·测试
大话性能4 天前
使用枚举(Enum)提升你的python代码质量
测试
公子测试4 天前
Trae版本更新 | 测试小白使用trae完成jira和飞书联动
测试
哔哩哔哩技术4 天前
B站端侧DCDN容灾演练实践
测试·cdn
WebInfra5 天前
🔥 Midscene 重磅更新:支持 AI 驱动的 Android 自动化
android·前端·测试