Jest工作原理
Jest 是一个流行的 JavaScript 测试框架,特别适用于 React 项目,但它也可以用来测试任何 JavaScript 代码。Jest 能够执行用 JavaScript 编写的测试文件的原因在于其设计和内部工作原理。下面是 Jest 的工作原理及其内部机制的详细解释,大致分为6部分
初始化:
Jest 在启动时会读取配置文件(如 jest.config.js)以及命令行参数来初始化自身的配置。
它会设置全局环境,包括全局变量、钩子函数(如 beforeEach、afterEach)等。
发现测试文件:
Jest 会遍历指定的目录,使用配置文件中定义的模式(如 **/__tests__/**/*.js?(x) 或 **/?(*.)+(spec|test).js?(x))来发现测试文件。
这些文件通常是以 .test.js 或 .spec.js 结尾,或者位于 __tests__ 目录下。
编译和转换:
对于使用现代 JavaScript 语法(如 ES6、ES7、JSX)的测试文件,Jest 会使用 Babel 或其他编译工具将其转换为兼容的 JavaScript 代码。Jest 内部集成了 Babel,能够自动识别并转换这些语法。
隔离环境执行:
Jest 为每个测试文件创建一个独立的沙盒环境。这个环境隔离了全局变量和模块缓存,确保测试之间不会相互干扰。通过 jest-runtime 模块,Jest 能够在这个隔离环境中加载并执行测试文件。
执行测试:
Jest 通过导入测试文件并执行其中的测试函数(如 test 或 it 函数)来运行测试。
Jest 会跟踪每个测试的结果,包括成功、失败、跳过等信息。
报告和反馈:
测试执行完毕后,Jest 生成详细的测试报告,包括每个测试的结果、执行时间、失败的断言信息等。Jest 还支持代码覆盖率报告,帮助开发者了解测试覆盖的代码范围。
这些模块中,隔离测试环境执行测试文件是很核心的模块,那么jest如何实现隔离环境以及执行测试的呢?实现这些的是jest-runtime模块,jest-runtime 通过 vm 模块(Node.js的虚拟机模块)在沙盒环境中执行测试文件。创建一个独立的执行上下文,并在其中运行测试代码,确保测试文件与主进程隔离。另外,jest-runtime 还提供了一组全局变量(如 test、expect、beforeEach、afterEach 等)供测试文件使用。这里使用到了vm模块,那什么是node.js的vm模块呢?
Node.js的vm模块
Node.js 的 vm 模块允许在 V8 虚拟机的上下文中编译和运行代码。它提供了几种 API来创建独立的执行环境(沙盒),用于隔离代码执行。这在以下场景中非常有用:
执行不信任的代码:通过沙盒环境执行用户输入的代码,以防止代码影响到主应用程序。
插件系统:动态加载和执行插件代码。
测试:在隔离的环境中运行测试代码,防止全局状态污染。
动态代码生成:根据运行时数据动态生成并执行代码。
利用vm编译和执行代码非常简单,具体代码如下图所示,当需要执行某段代码的时候,将code的字符串传入runInSandbox即可。
import { Script, createContext } from 'vm';
import assert from 'assert';
export const runInSandbox = (code, context = {}) => {
const sandbox = createContext({
...context,
assert,
console,
setTimeout,
setInterval,
clearTimeout,
clearInterval,
Buffer,
process,
});
const script = new Script(code);
script.runInContext(sandbox);
};
上面的的代码中,主要是用了vm模块提供Script对象和createContext方法。Script 对象是 vm 模块中的一个核心组件,它允许你编译并运行一段 JavaScript 代码。script.runInContext(context) 方法,可以在指定的上下文中运行编译后的代码。上下文是通过 vm.createContext 创建的。createContext中主要定义两种变量,全局变量和用户自定义变量。全局变量主要包括如下对象:
console: 提供日志记录功能,使沙盒内的代码能够输出日志信息。
setTimeout, setInterval, clearTimeout, clearInterval: 这些计时器函数允许沙盒内的代码使用异步操作。
Buffer: 使沙盒内的代码能够处理二进制数据。
process: 提供关于当前 Node.js 进程的信息,但通常会对其做一些限制,以防止恶意代码影响主进程。
用户自定义变量和方法主要包括:
测试框架相关的函数:例如 test 和 expect,用于定义和执行测试。
自定义的 require 函数:用于加载模块,确保沙盒内的代码只能访问允许的模块。
如何从0构建一款类jest的工具
初始化项目
mkdir custom-jest-runtime
cd custom-jest-runtime
npm init -y
npm install @babel/core @babel/preset-env
npm install expect
实现文件发现和收集逻辑
下面的代码中遍历目录中所有以.test.js结尾的文件,并将所有文件path进行收集存储。
import { readdirSync, statSync } from 'fs';
import { join } from 'path';
export const findTestFiles = (dir, testFiles = []) => {
const files = readdirSync(dir);
files.forEach((file) => {
const filePath = join(dir, file);
if (statSync(filePath).isDirectory()) {
findTestFiles(filePath, testFiles);
} else if (file.endsWith('.test.js')) {
testFiles.push(filePath);
}
});
return testFiles;
};
创建沙盒运行环境
这里使用node.js中的vm模块实现在沙盒环境中完成测试的编译的执行
import { Script, createContext } from 'vm';
import assert from 'assert';
export const runInSandbox = (code, context = {}) => {
const sandbox = createContext({
...context,
assert,
console,
setTimeout,
setInterval,
clearTimeout,
clearInterval,
Buffer,
process,
});
const script = new Script(code);
script.runInContext(sandbox);
};
模块解析和加载
使用 Babel 转换现代 JavaScript 代码,保证不同版本Js代码兼容性。
import * as babel from '@babel/core';
import { readFileSync } from 'fs';
export const loadModule = (filePath) => {
const code = readFileSync(filePath, 'utf8');
const { code: transformedCode } = babel.transformSync(code, {
presets: ['@babel/preset-env'],
});
return transformedCode;
};
执行测试文件逻辑
在runTestFile中,首先调用前面封装的loadModule来对测试文件内容进行转换,以兼容不同版本的js代码,接着在context上下文中自定义了test对象,这样当code中包含test的时候,就能进行识别。最后调用runInSandbox执行。
import { runInSandbox } from './sandbox.js';
import { loadModule } from './moduleLoader.js';
export const runTestFile = (testFile) => {
const code = loadModule(testFile);
const context = {
test: (name, fn) => {
try {
fn();
console.log(`Test passed: ${name}`);
} catch (error) {
console.log(`Test failed: ${name}`);
console.error(error);
}
},
};
runInSandbox(code, context);
};
//index.js入口文件
import { findTestFiles } from './fileFinder.js';
import { runTestFile } from './testRuntime.js';
const testFiles = findTestFiles(new URL('./tests', import.meta.url).pathname);
testFiles.forEach(runTestFile);
在tests目录下编写一个测试脚本,如下所示:
const sum = (a, b) => a + b;
test('adds 1 + 2 to equal 3', () => {
assert.strictEqual(sum(1, 2), 3);
});
运行index.js脚本(node index.js),得到如下结果,说明执行成功。
以上就是实现一款类似jest框架的过程。jest框架本身会比这个复杂很大,它自身又集成了其他一些工具,例如expect包等。