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)
  }
}
​

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

所有文件都测试完毕

以下内容等待更新...

相关推荐
得物技术15 小时前
搜索 C++ 引擎回归能力建设:从自测到工程化准出|得物技术
c++·后端·测试
阁老2 天前
pytest中集成allure打造更好的自动化测试报告
测试
软件测试同学2 天前
软件测试术语分享: 验收测试驱动开发 | Acceptance Test Driven Development
测试
用户962377954485 天前
VulnHub DC-1 靶机渗透测试笔记
笔记·测试
数据组小组5 天前
免费数据库管理工具深度横评:NineData 社区版、Bytebase 社区版、Archery,2026 年开发者该选哪个?
数据库·测试·数据库管理工具·数据复制·迁移工具·ninedata社区版·naivicat平替
Apifox6 天前
【测试套件】当用户说“我只想跑 P0 用例”时,我们到底在说什么
单元测试·测试·ab测试
阁老9 天前
pytest测试框架:如何确保登录模块先执行并共享登录状态
测试
_志哥_9 天前
Superpowers 技术指南:让 AI 编程助手拥有超能力
人工智能·ai编程·测试
FliPPeDround12 天前
浏览器扩展 E2E 测试的救星:vitest-environment-web-ext 让你告别繁琐配置
e2e·浏览器·测试
Apifox12 天前
Apifox 2 月更新|MCP Client 调试体验优化、测试套件持续升级、支持公用测试数据、测试报告优化
前端·后端·测试