Runner API
WARNING
这是一个高级 API。如果你只是想运行测试,可能不需要使用它。它主要供库作者使用。
你可以在配置文件中使用 runner
选项指定测试运行器的路径。该文件应默认导出一个类构造函数,该类需实现以下方法:
export interface VitestRunner {
/**
* 在实际收集和运行测试之前,此方法会首先被调用。
*/
onBeforeCollect?: (paths: string[]) => unknown;
/**
* 在收集测试之后,以及 `onBeforeRun` 之前调用。
*/
onCollected?: (files: File[]) => unknown;
/**
* 当测试运行器应该取消下一次测试运行时调用。
* 运行器应监听此方法,并在其被调用时,在 `onBeforeRunSuite` 和 `onBeforeRunTask` 中将测试和套件标记为跳过。
*/
onCancel?: (reason: CancelReason) => unknown;
/**
* 在运行单个测试之前调用。此时尚未包含 `result`。
*/
onBeforeRunTask?: (test: TaskPopulated) => unknown;
/**
* 在实际运行测试函数之前调用。此时已包含带有 `state` 和 `startTime` 的 `result`。
*/
onBeforeTryTask?: (
test: TaskPopulated,
options: { retry: number; repeats: number }
) => unknown;
/**
* 在结果和状态设置完成后调用。
*/
onAfterRunTask?: (test: TaskPopulated) => unknown;
/**
* 在运行测试函数之后立即调用。此时尚未包含新状态。如果测试函数抛出异常,则不会调用此方法。
*/
onAfterTryTask?: (
test: TaskPopulated,
options: { retry: number; repeats: number }
) => unknown;
/**
* 在运行单个套件之前调用。此时尚未包含 `result`。
*/
onBeforeRunSuite?: (suite: Suite) => unknown;
/**
* 在运行单个套件之后调用。此时已包含状态和结果。
*/
onAfterRunSuite?: (suite: Suite) => unknown;
/**
* 如果已定义,此方法将替代 Vitest 通常的套件分区和处理。
* `before` 和 `after` 钩子不会被忽略。
*/
runSuite?: (suite: Suite) => Promise<void>;
/**
* 如果已定义,此方法将替代 Vitest 通常的处理。如果你有自定义测试函数,此方法将非常有用。
* `before` 和 `after` 钩子不会被忽略。
*/
runTask?: (test: TaskPopulated) => Promise<void>;
/**
* 当任务更新时调用。与 reporter 中的 `onTaskUpdate` 作用相同,但此方法在与测试相同的线程中运行。
*/
onTaskUpdate?: (
task: [string, TaskResult | undefined, TaskMeta | undefined][]
) => Promise<void>;
/**
* 在运行收集到的路径中的所有测试之前调用。
*/
onBeforeRunFiles?: (files: File[]) => unknown;
/**
* 在运行收集到的路径中的所有测试之后立即调用。
*/
onAfterRunFiles?: (files: File[]) => unknown;
/**
* 当定义新的测试上下文时调用。如果你想向该上下文添加自定义属性,此方法将非常有用。
* 如果你只想通过 runner 定义自定义上下文,请考虑改用 `setupFiles` 中的 `beforeAll`。
*/
extendTaskContext?: (context: TestContext) => TestContext;
/**
* 当导入某些文件时调用。可在两种情况下调用:收集测试和导入设置文件。
*/
importFile: (filepath: string, source: VitestRunnerImportSource) => unknown;
/**
* 当 `test.extend` 与 `{ injected: true }` 一起使用时,runner 尝试获取值时调用的函数。
*/
injectValue?: (key: string) => unknown;
/**
* 公开可用的配置信息。
*/
config: VitestRunnerConfig;
/**
* 当前池的名称。它可能影响服务器端堆栈跟踪的推断方式。
*/
pool?: string;
}
实例化此类时,Vitest 会传入其配置,你应将其作为 config
属性公开:
import type { RunnerTestFile } from 'vitest';
import type { VitestRunner, VitestRunnerConfig } from 'vitest/suite';
import { VitestTestRunner } from 'vitest/runners';
class CustomRunner extends VitestTestRunner implements VitestRunner {
public config: VitestRunnerConfig;
constructor(config: VitestRunnerConfig) {
this.config = config;
}
onAfterRunFiles(files: RunnerTestFile[]) {
console.log('finished running', files);
}
}
export default CustomRunner;
WARNING
Vitest 还会将 ViteNodeRunner
的实例注入为 __vitest_executor
属性。你可以使用它在 importFile
方法中处理文件(这是 TestRunner
和 BenchmarkRunner
的默认行为)。
ViteNodeRunner
暴露了 executeId
方法,用于在 Vite 友好的环境中导入测试文件。这意味着它将在运行时解析导入并转换文件内容,以便 Node 能够理解:
export default class Runner {
async importFile(filepath: string) {
await this.__vitest_executor.executeId(filepath);
}
}
WARNING
如果你没有自定义运行器,或者未定义 runTask
方法,Vitest 将尝试自动检索任务。如果你没有通过 setFn
添加函数,它将失败。
TIP
快照支持和其他一些功能依赖于运行器。如果你不想失去这些功能,可以将你的运行器扩展自 vitest/runners
中导入的 VitestTestRunner
。此外,如果你想扩展基准测试功能,它还暴露了 BenchmarkNodeRunner
。
任务
WARNING
“Runner Tasks API”是实验性的,主要应仅在测试运行时使用。Vitest 还暴露了“Reported Tasks API”,在主线程中工作时(例如在 reporter 内部)应优先使用此 API。
团队目前正在讨论未来是否应以“Reported Tasks”替换“Runner Tasks”。
套件和测试在内部统称为 tasks
。Vitest 运行器在收集任何测试之前会初始化一个 File
任务——它是一个 Suite
的扩展,并带有一些附加属性。它在每个任务(包括 File
)上都作为 file
属性提供。
interface File extends Suite {
/**
* 文件所属的池的名称。
* @default 'forks'
*/
pool?: string;
/**
* 文件的 UNIX 格式路径。
*/
filepath: string;
/**
* 文件所属测试项目的名称。
*/
projectName: string | undefined;
/**
* 收集文件中所有测试所花费的时间。
* 此时间还包括导入所有文件依赖项所花费的时间。
*/
collectDuration?: number;
/**
* 导入设置文件所花费的时间。
*/
setupDuration?: number;
}
每个套件都包含一个 tasks
属性,该属性在收集阶段被填充。这对于从上到下遍历任务树是很有用的。
interface Suite extends TaskBase {
type: 'suite';
/**
* 文件任务。它是该文件的根任务。
*/
file: File;
/**
* 属于该套件的任务数组。
*/
tasks: Task[];
}
每个任务都包含一个 suite
属性,该属性引用了其所在的套件。如果 test
或 describe
在顶层初始化,它们将不包含 suite
属性(该属性不会等于 file
!)。File
也从不包含 suite
属性。这对于自底向上遍历任务是很有用的。
interface Test<ExtraContext = object> extends TaskBase {
type: 'test';
/**
* 将传递给测试函数的上下文。
*/
context: TestContext & ExtraContext;
/**
* 文件任务。它是该文件的根任务。
*/
file: File;
/**
* 任务是否因调用 `context.skip()` 而被跳过。
*/
pending?: boolean;
/**
* 任务即使失败是否也应被视为成功。如果任务失败,它将被标记为通过。
*/
fails?: boolean;
/**
* 存储 Promise(来自异步 expect),以便在测试完成前等待它们。
*/
promises?: Promise<any>[];
}
每个任务都可以包含一个 result
字段。套件仅当在套件回调或 beforeAll
/afterAll
回调中抛出的错误阻止其收集测试时,才能包含此字段。测试在调用其回调后总是包含此字段——state
和 errors
字段会根据结果而存在。如果在 beforeEach
或 afterEach
回调中抛出错误,则该错误将存在于 task.result.errors
中。
export interface TaskResult {
/**
* 任务的状态。在收集期间继承 `task.mode`。
* 当任务完成时,其状态将变为 `pass` 或 `fail`。
* - **pass**: 任务成功运行
* - **fail**: 任务失败
*/
state: TaskState;
/**
* 任务执行期间发生的错误。如果 `expect.soft()` 多次失败,可能会出现多个错误。
*/
errors?: ErrorWithDiff[];
/**
* 任务运行所花费的毫秒数。
*/
duration?: number;
/**
* 任务开始运行时的毫秒时间。
*/
startTime?: number;
/**
* 任务完成后堆的字节大小。
* 仅当 `logHeapUsage` 选项已设置且 `process.memoryUsage` 已定义时可用。
*/
heap?: number;
/**
* 与此任务相关的钩子状态。在报告期间非常有用。
*/
hooks?: Partial<
Record<'afterAll' | 'beforeAll' | 'beforeEach' | 'afterEach', TaskState>
>;
/**
* 任务重试的次数。任务仅在其失败且 `retry` 选项已设置时重试。
*/
retryCount?: number;
/**
* 任务重复的次数。任务仅在 `repeats` 选项已设置时重复。此数字也包含 `retryCount`。
*/
repeatCount?: number;
}
你的任务函数
Vitest 暴露了 createTaskCollector
工具,用于创建你自己的 test
方法。它的行为与测试相同,但在收集期间会调用一个自定义方法。
任务是套件中的一个对象。它会自动添加到当前套件中,通过 suite.task
方法:
import { createTaskCollector, getCurrentSuite } from 'vitest/suite';
export { afterAll, beforeAll, describe } from 'vitest';
// 此函数将在收集阶段调用:
// 不要在此处调用函数处理程序,而应将其添加到套件任务中
// 通过 "getCurrentSuite().task()" 方法
// 注意:createTaskCollector 提供对 "todo"/"each"/... 的支持
export const myCustomTask = createTaskCollector(function (name, fn, timeout) {
getCurrentSuite().task(name, {
...this, // 以便正确跟踪 "todo"/"skip"/... 等
meta: {
customPropertyToDifferentiateTask: true,
},
handler: fn,
timeout,
});
});
import { afterAll, beforeAll, describe, myCustomTask } from './custom.js';
import { gardener } from './gardener.js';
describe('take care of the garden', () => {
beforeAll(() => {
gardener.putWorkingClothes();
});
myCustomTask('weed the grass', () => {
gardener.weedTheGrass();
});
myCustomTask.todo('mow the lawn', () => {
gardener.mowerTheLawn();
});
myCustomTask('water flowers', () => {
gardener.waterFlowers();
});
afterAll(() => {
gardener.goHome();
});
});
vitest ./garden/tasks.test.js