关于javascript:手把手教你如何实现一个单元测试框架

50次阅读

共计 14969 个字符,预计需要花费 38 分钟才能阅读完成。

目录

本文次要给大家深刻理解 Jest 背地的运行原理,并从零开始简略实现一个 Jest 单元测试的框架,不便理解单元测试引擎是如何工作的,Jest 编写单测置信咱们曾经很相熟了,但 Jest 是如何工作的咱们可能还很生疏,那让咱们一起走进 Jest 心田,一起探索单元测试引擎是如何工作的。

先附上 Jest 外围引擎的代码实现给有须要的同学,欢送关注和交换:https://github.com/wscats/jes…

  • 什么是 Jest
  • 测试意味着什么
  • 我怎么晓得要测试什么
  • 测试块,断言和匹配器

    • 如何实现测试块
    • 如何实现断言和匹配器
    • CLI 和配置
  • 模仿

    • 怎么模仿一个函数
  • 执行环境

    • 作用域隔离
    • V8 虚拟机
    • 运行单测回调
    • 钩子函数
  • 生成报告
  • jest-cli
  • jest-config
  • jest-haste-map
  • jest-runner
  • jest-environment-node
  • jest-circus
  • jest-runtime
  • 最初 & 源码

什么是 Jest

Jest 是 Facebook 开发的 Javascript 测试框架,用于创立、运行和编写测试的 JavaScript 库。

Jest 作为 NPM 包公布,能够装置并运行在任何 JavaScript 我的项目中。Jest 是目前前端最风行的测试库之一。

测试意味着什么

在技术术语中,测试意味着查看咱们的代码是否满足某些冀望。例如:一个名为求和 (sum) 函数应该返回给定一些运算后果的预期输入。

有许多类型的测试,很快你就会被术语吞没,但长话短说的测试分为三大类:

  • 单元测试
  • 集成测试
  • E2E 测试

我怎么晓得要测试什么

在测试方面,即便是最简略的代码块也可能使初学者也可能会蛊惑。最常见的问题是“我怎么晓得要测试什么?”。

如果您正在编写网页,一个好的出发点是测试应用程序的每个页面和每个用户交互。然而网页其实也须要测试的函数和模块等代码单元组成。

大多数时候有两种状况:

  • 你继承遗留代码,其自带没有测试
  • 你必须凭空实现一个新性能

那该怎么办?对于这两种状况,你能够通过将测试视为:查看该函数是否产生预期后果。最典型的测试流程如下所示:

  • 导入要测试的函数
  • 给函数一个输出
  • 定义冀望的输入
  • 查看函数是否产生预期的输入

个别,就这么简略。把握以下外围思路,编写测试将不再可怕:

输出 -> 预期输入 -> 断言后果。

测试块,断言和匹配器

咱们将创立一个简略的 Javascript 函数代码,用于 2 个数字的加法,并为其编写相应的基于 Jest 的测试

const sum = (a, b) => a + b;

当初,为了测试在同一个文件夹中创立一个测试文件,命名为 test.spec.js,这非凡的后缀是 Jest 的约定,用于查找所有的测试文件。咱们还将导入被测函数,以便执行测试中的代码。Jest 测试遵循 BDD 格调的测试,每个测试都应该有一个次要的 test 测试块,并且能够有多个测试块,当初能够为 sum 办法编写测试块,这里咱们编写一个测试来增加 2 个数字并验证预期后果。咱们将提供数字为 1 和 2,并冀望输入 3。

test 它须要两个参数:一个用于形容测试块的字符串,以及一个用于包装理论测试的回调函数。expect 包装指标函数,并联合匹配器 toBe 用于查看函数计算结果是否合乎预期。

这是残缺的测试:

test("sum test", () => {expect(sum(1, 2)).toBe(3);
});

咱们察看下面代码有发现有两点:

  • test 块是独自的测试块,它领有形容和划分范畴的作用,即它代表咱们要为该计算函数 sum 所编写测试的通用容器。
  • expect 是一个断言,该语句应用输出 1 和 2 调用被测函数中的 sum 办法,并冀望输入 3。
  • toBe 是一个匹配器,用于查看期望值,如果不合乎预期后果则应该抛出异样。

如何实现测试块

测试块其实并不简单,最简略的实现不过如下,咱们须要把测试包装理论测试的回调函数存起来,所以封装一个 dispatch 办法接管命令类型和回调函数:

const test = (name, fn) => {dispatch({ type: "ADD_TEST", fn, name});
};

咱们须要在全局创立一个 state 保留测试的回调函数,测试的回调函数应用一个数组存起来。

global["STATE_SYMBOL"] = {testBlock: [],
};

dispatch 办法此时只须要甄别对应的命令,并把测试的回调函数存进全局的 state 即可。

const dispatch = (event) => {const { fn, type, name} = event;
  switch (type) {
    case "ADD_TEST":
      const {testBlock} = global["STATE_SYMBOL"];
      testBlock.push({fn, name});
      break;
  }
};

如何实现断言和匹配器

断言库也实现也很简略,只须要封装一个函数裸露匹配器办法满足以下公式即可:

expect(A).toBe(B)

这里咱们实现 toBe 这个罕用的办法,当后果和预期不相等,抛出谬误即可:

const expect = (actual) => ({toBe(expected) {if (actual !== expected) {throw new Error(`${actual} is not equal to ${expected}`);
        }
    }
};

理论在测试块中会应用 try/catch 捕捉谬误,并打印堆栈信息方面定位问题。

在简略状况下,咱们也能够应用 Node 自带的 assert 模块进行断言,当然还有很多更简单的断言办法,实质上原理都差不多。

CLI 和配置

编写完测试之后,咱们则须要在命令行中输出命令运行单测,失常状况下,命令相似如下:

node jest xxx.spec.js

这里实质是解析命令行的参数。

const testPath = process.argv.slice(2)[0];
const code = fs.readFileSync(path.join(process.cwd(), testPath)).toString();

简单的状况可能还须要读取本地的 Jest 配置文件的参数来更改执行环境等,Jest 在这里应用了第三方库 yargs execachalk 等来解析执行并打印命令。

模仿

在简单的测试场景,咱们肯定绕不开一个 Jest 术语:模仿(mock)

在 Jest 文档中,咱们能够找到 Jest 对模仿有以下形容:”模仿函数通过抹去函数的理论实现、捕捉对函数的调用,以及在这些调用中传递的参数,使测试代码之间的链接变得容易“

简而言之,能够通过将以下代码片段调配给函数或依赖项来创立模仿:

jest.mock("fs", {readFile: jest.fn(() => "wscats"),
});

这是一个简略模仿的示例,模仿了 fs 模块 readFile 函数在测试特定业务逻辑的返回值。

怎么模仿一个函数

接下来咱们就要钻研一下如何实现,首先是 jest.mock,它第一个参数承受的是模块名或者模块门路,第二个参数是该模块对外裸露办法的具体实现

const jest = {mock(mockPath, mockExports = {}) {const path = require.resolve(mockPath, { paths: ["."] });
    require.cache[path] = {
      id: path,
      filename: path,
      loaded: true,
      exports: mockExports,
    };
  },
};

咱们计划其实跟下面的 test 测试块实现统一,只须要把具体的实现办法找一个中央存起来即可,等后续真正应用改模块的时候替换掉即可,所以咱们把它存到 require.cache 外面,当然咱们也能够存到全局的 state 中。

jest.fn 的实现也不难,这里咱们应用一个闭包 mockFn 把替换的函数和参数给存起来,不便后续测试检查和统计调用数据。

const jest = {fn(impl = () => {}) {const mockFn = (...args) => {mockFn.mock.calls.push(args);
      return impl(...args);
    };
    mockFn.originImpl = impl;
    mockFn.mock = {calls: [] };
    return mockFn;
  },
};

执行环境

有些同学可能留意到了,在测试框架中,咱们并不需要手动引入 testexpectjest 这些函数,每个测试文件能够间接应用,所以咱们这里须要发明一个注入这些办法的运行环境。

作用域隔离

因为单测文件运行时候须要作用域隔离。所以在设计上测试引擎是跑在 node 全局作用域下,而测试文件的代码则跑在 node 环境里的 vm 虚拟机部分作用域中。

  • 全局作用域 global
  • 部分作用域 context

两个作用域通过 dispatch 办法实现通信。

dispatch 在 vm 部分作用域下收集测试块、生命周期和测试报告信息到 node 全局作用域 STATE_SYMBOL 中,所以 dispatch 次要波及到以下各种通信类型:

  • 测试块

    • ADD_TEST
  • 生命周期

    • BEFORE_EACH
    • BEFORE_ALL
    • AFTER_EACH
    • AFTER_ALL
  • 测试报告

    • COLLECT_REPORT

V8 虚拟机

既然万事俱备只欠东风,咱们只须要给 V8 虚拟机注入测试所需的办法,即注入测试作用域即可。

const context = {console: console.Console({ stdout: process.stdout, stderr: process.stderr}),
  jest,
  expect,
  require,
  test: (name, fn) => dispatch({type: "ADD_TEST", fn, name}),
};

注入完作用域,咱们就能够让测试文件的代码在 V8 虚拟机中跑起来,这里我传入的代码是曾经解决成字符串的代码,Jest 这里会在这里做一些代码加工,平安解决和 SourceMap 补缀等操作,咱们示例就不须要搞那么简单了。

vm.runInContext(code, context);

在代码执行的前后能够应用时间差算出单测的运行工夫,Jest 还会在这里预评估单测文件的大小数量等,决定是否启用 Worker 来优化执行速度

const start = new Date();
const end = new Date();
log("\x1b[32m%s\x1b[0m", `Time: ${end - start}ms`);

运行单测回调

V8 虚拟机执行结束之后,全局的 state 就会收集到测试块中所有包装好的测试回调函数,咱们最初只须要把所有的这些回调函数遍历取出来,并执行。

testBlock.forEach(async (item) => {const { fn, name} = item;
  try {await fn.apply(this);
    log("\x1b[32m%s\x1b[0m", `√ ${name} passed`);
  } catch {log("\x1b[32m%s\x1b[0m", `× ${name} error`);
  }
});

钩子函数

咱们还能够在单测执行过程中退出生命周期,例如 beforeEachafterEachafterAllbeforeAll 等钩子函数。

在下面的基础架构上减少钩子函数,其实就是在执行 test 的每个过程中注入对应回调函数,比方 beforeEach 就是放在 testBlock 遍历执行测试函数前,afterEach 就是放在 testBlock 遍历执行测试函数后,十分的简略,只须要地位放对就能够裸露任何期间的钩子函数。

testBlock.forEach(async (item) => {const { fn, name} = item;
  +beforeEachBlock.forEach(async (beforeEach) => await beforeEach());
  await fn.apply(this);
  +afterEachBlock.forEach(async (afterEach) => await afterEach());
});

beforeAllafterAll 就能够放在,testBlock 所有测试运行结束前和后。

beforeAllBlock.forEach(async (beforeAll) => await beforeAll());
testBlock.forEach(async (item) => {})
afterAllBlock.forEach(async (afterAll) => await afterAll());

生成报告

当单测执行完后,能够收集胜利和捕获谬误的信息集,

try {dispatch({ type: "COLLECT_REPORT", name, pass: 1});
    log("\x1b[32m%s\x1b[0m", `√ ${name} passed`);
} catch (error) {dispatch({ type: "COLLECT_REPORT", name, pass: 0});
    log("\x1b[32m%s\x1b[0m", `× ${name} error`);
}

而后劫持 log 的输入流,让具体的后果打印在终端上,也能够配合 IO 模块在本地生成报告。

const {reports} = global["STATE_SYMBOL"];
const pass = reports.reduce((pre, next) => pre.pass + next.pass);
log("\x1b[32m%s\x1b[0m", `All Tests: ${pass}/${reports.length} passed`);

至此,咱们就实现了一个简略的 Jest 测试框架的外围局部,以上局部根本实现了测试块、断言、匹配器、CLI 配置、函数模仿、应用虚拟机及作用域和生命周期钩子函数等,咱们能够在此基础上,丰盛断言办法,匹配器和反对参数配置,当然理论 Jest 的实现会更简单,我只提炼了比拟要害的局部,所以附上自己读 Jest 源码的集体笔记供大家参考。

jest-cli

下载 Jest 源码,根目录下执行

yarn
npm run build

它实质跑的是 script 文件夹的两个文件 build.js 和 buildTs.js:

"scripts": {
    "build": "yarn build:js && yarn build:ts",
    "build:js": "node ./scripts/build.js",
    "build:ts": "node ./scripts/buildTs.js",
}

build.js 实质上是应用了 babel 库,在 package/xxx 包新建一个 build 文件夹,而后应用 transformFileSync 把文件生成到 build 文件夹外面:

const transformed = babel.transformFileSync(file, options).code;

而 buildTs.js 实质上是应用了 tsc 命令,把 ts 文件编译到 build 文件夹中,应用 execa 库来执行命令:

const args = ["tsc", "-b", ...packagesWithTs, ...process.argv.slice(2)];
await execa("yarn", args, { stdio: "inherit"});

执行胜利会显示如下,它会帮你把 packages 文件夹下的所有文件 js 文件和 ts 文件编译到所在目录的 build 文件夹下:

接下来咱们能够启动 jest 的命令:

npm run jest
# 等价于
# node ./packages/jest-cli/bin/jest.js

这里能够依据传入的不同参数做解析解决,比方:

npm run jest -h
node ./packages/jest-cli/bin/jest.js /path/test.spec.js

就会执行 jest.js 文件,而后进入到 build/cli 文件中的 run 办法,run 办法会对命令中各种的参数做解析,具体原理是 yargs 库配合 process.argv 实现

const importLocal = require("import-local");

if (!importLocal(__filename)) {if (process.env.NODE_ENV == null) {process.env.NODE_ENV = "test";}

  require("../build/cli").run();}

jest-config

当获取各种命令参数后,就会执行 runCLI 外围的办法,它是 @jest/core -> packages/jest-core/src/cli/index.ts 库的外围办法。

import {runCLI} from "@jest/core";
const outputStream = argv.json || argv.useStderr ? process.stderr : process.stdout;
const {results, globalConfig} = await runCLI(argv, projects);

runCLI 办法中会应用方才命令中解析好的传入参数 argv 来配合 readConfigs 办法读取配置文件的信息,readConfigs 来自于 packages/jest-config/src/index.ts,这里会有 normalize 填补和初始化一些默认配置好的参数,它的默认参数在 packages/jest-config/src/Defaults.ts 文件中记录,比方:如果只运行 js 单测,会默认设置 require.resolve('jest-runner') 为运行单测的 runner,还会配合 chalk 库生成 outputStream 输入内容到控制台。

这里顺便提一下引入 jest 引入模块的原理思路,这里先会 require.resolve(moduleName) 找到模块的门路,并把门路存到配置外面,而后应用工具库 packages/jest-util/src/requireOrImportModule.tsrequireOrImportModule 办法调用封装好的原生 import/reqiure 办法配合配置文件中的门路把模块取出来。

  • globalConfig 来自于 argv 的配置
  • configs 来自于 jest.config.js 的配置
const {globalConfig, configs, hasDeprecationWarnings} = await readConfigs(
  argv,
  projects
);

if (argv.debug) {/*code*/}
if (argv.showConfig) {/*code*/}
if (argv.clearCache) {/*code*/}
if (argv.selectProjects) {/*code*/}

jest-haste-map

jest-haste-map 用于获取我的项目中的所有文件以及它们之间的依赖关系,它通过查看 import/require 调用来实现这一点,从每个文件中提取它们并构建一个映射,其中蕴含每个文件及其依赖项,这里的 Haste 是 Facebook 应用的模块零碎,它还有一个叫做 HasteContext 的货色,因为它有 HastFS(Haste 文件系统),HastFS 只是零碎中文件的列表以及与之关联的所有依赖项,它是一种地图数据结构,其中键是门路,值是元数据,这里生成的 contexts 会始终被沿用到 onRunComplete 阶段。

const {contexts, hasteMapInstances} = await buildContextsAndHasteMaps(
  configs,
  globalConfig,
  outputStream
);

jest-runner

_run10000 办法中会依据配置信息 globalConfigconfigs 获取 contextscontexts 会存储着每个部分文件的配置信息和门路等,而后会带着回调函数 onComplete,全局配置 globalConfig 和作用域 contexts 进入 runWithoutWatch 办法。

接下来会进入 packages/jest-core/src/runJest.ts 文件的 runJest 办法中,这里会应用传过来的 contexts 遍历出所有的单元测试并用数组保存起来。

let allTests: Array<Test> = [];
contexts.map(async (context, index) => {const searchSource = searchSources[index];
  const matches = await getTestPaths(
    globalConfig,
    searchSource,
    outputStream,
    changedFilesPromise && (await changedFilesPromise),
    jestHooks,
    filter
  );
  allTests = allTests.concat(matches.tests);
  return {context, matches};
});

并应用 Sequencer 办法对单测进行排序

const Sequencer: typeof TestSequencer = await requireOrImportModule(globalConfig.testSequencer);
const sequencer = new Sequencer();
allTests = await sequencer.sort(allTests);

runJest 办法会调用一个要害的办法 packages/jest-core/src/TestScheduler.tsscheduleTests 办法。

const results = await new TestScheduler(
  globalConfig,
  {startRun},
  testSchedulerContext
).scheduleTests(allTests, testWatcher);

scheduleTests 办法会做很多事件,会把 allTests 中的 contexts 收集到 contexts 中,把 duration 收集到 timings 数组中,并在执行所有单测前订阅四个生命周期:

  • test-file-start
  • test-file-success
  • test-file-failure
  • test-case-result

接着把 contexts 遍历并用一个新的空对象 testRunners 做一些解决存起来,外面会调用 @jest/transform 提供的 createScriptTransformer 办法来解决引入的模块。

import {createScriptTransformer} from "@jest/transform";

const transformer = await createScriptTransformer(config);
const Runner: typeof TestRunner = interopRequireDefault(transformer.requireAndTranspileModule(config.runner)
).default;
const runner = new Runner(this._globalConfig, {
  changedFiles: this._context?.changedFiles,
  sourcesRelatedToTestsInChangedFiles: this._context?.sourcesRelatedToTestsInChangedFiles,
});
testRunners[config.runner] = runner;

scheduleTests 办法会调用 packages/jest-runner/src/index.tsrunTests 办法。

async runTests(tests, watcher, onStart, onResult, onFailure, options) {
  return await (options.serial
    ? this._createInBandTestRun(tests, watcher, onStart, onResult, onFailure)
    : this._createParallelTestRun(
        tests,
        watcher,
        onStart,
        onResult,
        onFailure
      ));
}

最终 _createParallelTestRun 或者 _createInBandTestRun 办法外面:

  • _createParallelTestRun

外面会有一个 runTestInWorker 办法,这个办法顾名思义就是在 worker 外面执行单测。

  • _createInBandTestRun 外面会执行 packages/jest-runner/src/runTest.ts 一个外围办法 runTest,而 runJest 外面就执行一个办法 runTestInternal,这外面会在执行单测前筹备十分多的货色,波及全局办法改写和引入和导出办法的劫持。
await this.eventEmitter.emit("test-file-start", [test]);
return runTest(
  test.path,
  this._globalConfig,
  test.context.config,
  test.context.resolver,
  this._context,
  sendMessageToJest
);

runTestInternal 办法中会应用 fs 模块读取文件的内容放入 cacheFS,缓存起来不便当前快读读取,比方前面如果文件的内容是 json 就能够间接在 cacheFS 读取,也会应用 Date.now 时间差计算耗时。

const testSource = fs().readFileSync(path, "utf8");
const cacheFS = new Map([[path, testSource]]);

runTestInternal 办法中会引入 packages/jest-runtime/src/index.ts,它会帮你缓存模块和读取模块并触发执行。

const runtime = new Runtime(
  config,
  environment,
  resolver,
  transformer,
  cacheFS,
  {
    changedFiles: context?.changedFiles,
    collectCoverage: globalConfig.collectCoverage,
    collectCoverageFrom: globalConfig.collectCoverageFrom,
    collectCoverageOnlyFrom: globalConfig.collectCoverageOnlyFrom,
    coverageProvider: globalConfig.coverageProvider,
    sourcesRelatedToTestsInChangedFiles: context?.sourcesRelatedToTestsInChangedFiles,
  },
  path
);

jest-environment-node

这里应用 @jest/console 包改写全局的 console,为了单测的文件代码块的 console 能顺利在 node 终端打印后果,配合 jest-environment-node 包,把全局的 environment.global 全副改写,不便后续在 vm 中能失去这些作用域的办法,实质上就是为 vm 的运行环境提供的作用域,为后续注入 global 提供便当,波及到改写的 global 办法有如下:

  • global.global
  • global.clearInterval
  • global.clearTimeout
  • global.setInterval
  • global.setTimeout
  • global.Buffer
  • global.setImmediate
  • global.clearImmediate
  • global.Uint8Array
  • global.TextEncoder
  • global.TextDecoder
  • global.queueMicrotask
  • global.AbortController

testConsole 实质上是应用 node 的 console 改写,不便后续笼罩 vm 作用域外面的 console 办法。

testConsole = new BufferedConsole();
const environment = new TestEnvironment(config, {
  console: testConsole,
  docblockPragmas,
  testPath: path,
});
// 真正改写 console 中央的办法
setGlobal(environment.global, "console", testConsole);

runtime 次要用这两个办法加载模块,先判断是否 ESM 模块,如果是,应用 runtime.unstable_importModule 加载模块并运行该模块,如果不是,则应用 runtime.requireModule 加载模块并运行该模块。

const esm = runtime.unstable_shouldLoadAsEsm(path);

if (esm) {await runtime.unstable_importModule(path);
} else {runtime.requireModule(path);
}

jest-circus

紧接着 runTestInternal 中的 testFramework 会承受传入的 runtime 调用单测文件运行,testFramework 办法来自于一个名字比拟有意思的库 packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts,其中 legacy-code-todo-rewrite 意思为 遗留代码待办事项重写jest-circus 次要会把全局 global 的一些办法进行重写,波及这几个:

  • afterAll
  • afterEach
  • beforeAll
  • beforeEach
  • describe
  • it
  • test

这里调用单测前会在 jestAdapter 函数中,也就是下面提到的 runtime.requireModule 加载 xxx.spec.js 文件,这里执行之前曾经应用 initialize 预设好了执行环境 globalssnapshotState,并改写 beforeEach,如果配置了 resetModulesclearMocksresetMocksrestoreMockssetupFilesAfterEnv 则会别离执行上面几个办法:

  • runtime.resetModules
  • runtime.clearAllMocks
  • runtime.resetAllMocks
  • runtime.restoreAllMocks
  • runtime.requireModule 或者 runtime.unstable_importModule

当运行完 initialize 办法初始化之后,因为 initialize 改写了全局的 describetest 等办法,这些办法都在 /packages/jest-circus/src/index.ts 这里改写,这里留神 test 办法外面有一个 dispatchSync 办法,这是一个要害的办法,这里会在全局保护一份 statedispatchSync 就是把 test 代码块外面的函数等信息存到 state 外面,dispatchSync 外面应用 name 配合 eventHandler 办法来批改 state,这个思路十分像 redux 外面的数据流。

const test: Global.It = () => {return (test = (testName, fn, timeout) => (testName, mode, fn, testFn, timeout) => {
    return dispatchSync({
      asyncError,
      fn,
      mode,
      name: "add_test",
      testName,
      timeout,
    });
  });
};

而单测 xxx.spec.js 即 testPath 文件会在 initialize 之后会被引入并执行,留神这里引入就会执行这个单测,因为单测 xxx.spec.js 文件外面按标准写,会有 testdescribe 等代码块,所以这个时候所有的 testdescribe 承受的回调函数都会被存到全局的 state 外面。

const esm = runtime.unstable_shouldLoadAsEsm(testPath);
if (esm) {await runtime.unstable_importModule(testPath);
} else {runtime.requireModule(testPath);
}

jest-runtime

这里的会先判断是否 esm 模块,如果是则应用 unstable_importModule 的形式引入,否则应用 requireModule 的形式引入,具体会进入上面吗这个函数。

this._loadModule(localModule, from, moduleName, modulePath, options, moduleRegistry);

\_loadModule 的逻辑只有三个次要局部

  • 判断是否 json 后缀文件,执行 readFile 读取文本,用 transformJson 和 JSON.parse 转格输入内容。
  • 判断是否 node 后缀文件,执行 require 原生办法引入模块。
  • 不满足上述两个条件的文件,执行 \_execModule 执行模块。

\_execModule 中会应用 babel 来转化 fs 读取到的源代码,这个 transformFile 就是 packages/jest-runtime/src/index.tstransform 办法。

const transformedCode = this.transformFile(filename, options);

\_execModule 中会应用 createScriptFromCode 办法调用 node 的原生 vm 模块来真正的执行 js,vm 模块承受平安的源代码,并用 V8 虚拟机配合传入的上下文来立刻执行代码或者延时执行代码,这里能够承受不同的作用域来执行同一份代码来运算出不同的后果,十分适合测试框架的应用,这里的注入的 vmContext 就是下面全局改写作用域蕴含 afterAll,afterEach,beforeAll,beforeEach,describe,it,test,所以咱们的单测代码在运行的时候就会失去领有注入作用域的这些办法。

const vm = require("vm");
const script = new vm().Script(scriptSourceCode, option);
const filename = module.filename;
const vmContext = this._environment.getVmContext();
script.runInContext(vmContext, {filename,});

当下面复写全局办法和保留好 state 之后,会进入到真正执行 describe 的回调函数的逻辑外面,在 packages/jest-circus/src/run.tsrun 办法外面,这里应用 getState 办法把 describe 代码块取出来,而后应用 _runTestsForDescribeBlock 执行这个函数,而后进入到 _runTest 办法,而后应用 _callCircusHook 执行前后的钩子函数,应用 _callCircusTest 执行。

const run = async (): Promise<Circus.RunResult> => {const { rootDescribeBlock} = getState();
  await dispatch({name: "run_start"});
  await _runTestsForDescribeBlock(rootDescribeBlock);
  await dispatch({name: "run_finish"});
  return makeRunResult(getState().rootDescribeBlock, getState().unhandledErrors);
};

const _runTest = async (test, parentSkipped) => {
  // beforeEach
  // test 函数块,testContext 作用域
  await _callCircusTest(test, testContext);
  // afterEach
};

这是钩子函数实现的外围地位,也是 Jest 性能的外围因素。

最初

最初附上 Jest 外围引擎的代码实现给有须要的同学,欢送关注和交换:https://github.com/wscats/jes…

本文可能帮忙大家了解 Jest 测试框架的外围实现和原理,感激大家急躁的浏览,心愿能带给您一丝帮忙或者启发 😁

正文完
 0