如何从0构建一款类jest工具

发布于:2024-06-26 ⋅ 阅读:(45) ⋅ 点赞:(0)

 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包等。


网站公告

今日签到

点亮在社区的每一天
去签到