Skip to content

Test Runner

WARNING

This is advanced API. If you are just running tests, you probably don't need this. It is primarily used by library authors.

You can specify a path to your test runner with the runner option in your configuration file. This file should have a default export with a class constructor implementing these methods:

ts
export interface VitestRunner {
  /**
   * First thing that's getting called before actually collecting and running tests.
   */
  onBeforeCollect?: (paths: string[]) => unknown
  /**
   * Called after collecting tests and before "onBeforeRun".
   */
  onCollected?: (files: File[]) => unknown

  /**
   * Called when test runner should cancel next test runs.
   * Runner should listen for this method and mark tests and suites as skipped in
   * "onBeforeRunSuite" and "onBeforeRunTask" when called.
   */
  onCancel?: (reason: CancelReason) => unknown

  /**
   * Called before running a single test. Doesn't have "result" yet.
   */
  onBeforeRunTask?: (test: TaskPopulated) => unknown
  /**
   * Called before actually running the test function. Already has "result" with "state" and "startTime".
   */
  onBeforeTryTask?: (test: TaskPopulated, options: { retry: number; repeats: number }) => unknown
  /**
   * Called after result and state are set.
   */
  onAfterRunTask?: (test: TaskPopulated) => unknown
  /**
   * Called right after running the test function. Doesn't have new state yet. Will not be called, if the test function throws.
   */
  onAfterTryTask?: (test: TaskPopulated, options: { retry: number; repeats: number }) => unknown

  /**
   * Called before running a single suite. Doesn't have "result" yet.
   */
  onBeforeRunSuite?: (suite: Suite) => unknown
  /**
   * Called after running a single suite. Has state and result.
   */
  onAfterRunSuite?: (suite: Suite) => unknown

  /**
   * If defined, will be called instead of usual Vitest suite partition and handling.
   * "before" and "after" hooks will not be ignored.
   */
  runSuite?: (suite: Suite) => Promise<void>
  /**
   * If defined, will be called instead of usual Vitest handling. Useful, if you have your custom test function.
   * "before" and "after" hooks will not be ignored.
   */
  runTask?: (test: TaskPopulated) => Promise<void>

  /**
   * Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests.
   */
  onTaskUpdate?: (task: [string, TaskResult | undefined][]) => Promise<void>

  /**
   * Called before running all tests in collected paths.
   */
  onBeforeRunFiles?: (files: File[]) => unknown
  /**
   * Called right after running all tests in collected paths.
   */
  onAfterRunFiles?: (files: File[]) => unknown
  /**
   * Called when new context for a test is defined. Useful, if you want to add custom properties to the context.
   * If you only want to define custom context with a runner, consider using "beforeAll" in "setupFiles" instead.
   *
   * This method is called for both "test" and "custom" handlers.
   *
   * @see https://vitest.dev/advanced/runner.html#your-task-function
   */
  extendTaskContext?: <T extends Test | Custom>(context: TaskContext<T>) => TaskContext<T>
  /**
   * Called, when certain files are imported. Can be called in two situations: when collecting tests and when importing setup files.
   */
  importFile: (filepath: string, source: VitestRunnerImportSource) => unknown
  /**
   * Publicly available configuration.
   */
  config: VitestRunnerConfig
}

When initiating this class, Vitest passes down Vitest config, - you should expose it as a config property.

WARNING

Vitest also injects an instance of ViteNodeRunner as __vitest_executor property. You can use it to process files in importFile method (this is default behavior of TestRunner and BenchmarkRunner).

ViteNodeRunner exposes executeId method, which is used to import test files in a Vite-friendly environment. Meaning, it will resolve imports and transform file content at runtime so that Node can understand it.

TIP

Snapshot support and some other features depend on the runner. If you don't want to lose it, you can extend your runner from VitestTestRunner imported from vitest/runners. It also exposes BenchmarkNodeRunner, if you want to extend benchmark functionality.

Tasks

Suites and tests are called tasks internally. Vitest runner initiates a File task before collecting any tests - this is a superset of Suite with a few additional properties. It is available on every task (including File) as a file property.

ts
interface File extends Suite {
  /**
   * The name of the pool that the file belongs to.
   * @default 'forks'
   */
  pool?: string
  /**
   * The path to the file in UNIX format.
   */
  filepath: string
  /**
   * The name of the workspace project the file belongs to.
   */
  projectName: string | undefined
  /**
   * The time it took to collect all tests in the file.
   * This time also includes importing all the file dependencies.
   */
  collectDuration?: number
  /**
   * The time it took to import the setup file.
   */
  setupDuration?: number
  /**
   * Whether the file is initiated without running any tests.
   * This is done to populate state on the server side by Vitest.
   */
  local?: boolean
}

Every suite has a tasks property that is populated during collection phase. It is useful to traverse the task tree from the top down.

ts
interface Suite extends TaskBase {
  type: 'suite'
  /**
   * File task. It's the root task of the file.
   */
  file: File
  /**
   * An array of tasks that are part of the suite.
   */
  tasks: Task[]
}

Every task has a suite property that references a suite it is located in. If test or describe are initiated at the top level, they will not have a suite property (it will not be equal to file!). File also never has a suite property. It is useful to travers the tasks from the bottom up.

ts
interface Test<ExtraContext = object> extends TaskBase {
  type: 'test'
  /**
   * Test context that will be passed to the test function.
   */
  context: TaskContext<Test> & ExtraContext & TestContext
  /**
   * File task. It's the root task of the file.
   */
  file: File
  /**
   * Whether the task was skipped by calling `t.skip()`.
   */
  pending?: boolean
  /**
   * Whether the task should succeed if it fails. If the task fails, it will be marked as passed.
   */
  fails?: boolean
  /**
   * Hooks that will run if the task fails. The order depends on the `sequence.hooks` option.
   */
  onFailed?: OnTestFailedHandler[]
  /**
   * Hooks that will run after the task finishes. The order depends on the `sequence.hooks` option.
   */
  onFinished?: OnTestFinishedHandler[]
  /**
   * Store promises (from async expects) to wait for them before finishing the test
   */
  promises?: Promise<any>[]
}

Every task can have a result field. Suites can only have this field if an error thrown within a suite callback or beforeAll/afterAll callbacks prevents them from collecting tests. Tests always have this field after their callbacks are called - the state and errors fields are present depending on the outcome. If an error was thrown in beforeEach or afterEach callbacks, the thrown error will be present in task.result.errors.

ts
export interface TaskResult {
  /**
   * State of the task. Inherits the `task.mode` during collection.
   * When the task has finished, it will be changed to `pass` or `fail`.
   * - **pass**: task ran successfully
   * - **fail**: task failed
   */
  state: TaskState
  /**
   * Errors that occurred during the task execution. It is possible to have several errors
   * if `expect.soft()` failed multiple times.
   */
  errors?: ErrorWithDiff[]
  /**
   * How long in milliseconds the task took to run.
   */
  duration?: number
  /**
   * Time in milliseconds when the task started running.
   */
  startTime?: number
  /**
   * Heap size in bytes after the task finished.
   * Only available if `logHeapUsage` option is set and `process.memoryUsage` is defined.
   */
  heap?: number
  /**
   * State of related to this task hooks. Useful during reporting.
   */
  hooks?: Partial<Record<'afterAll' | 'beforeAll' | 'beforeEach' | 'afterEach', TaskState>>
  /**
   * The amount of times the task was retried. The task is retried only if it
   * failed and `retry` option is set.
   */
  retryCount?: number
  /**
   * The amount of times the task was repeated. The task is repeated only if
   * `repeats` option is set. This number also contains `retryCount`.
   */
  repeatCount?: number
}

Your Task Function

Vitest exposes a Custom task type that allows users to reuse built-int reporters. It is virtually the same as Test, but has a type of 'custom'.

A task is an object that is part of a suite. It is automatically added to the current suite with a suite.task method:

js
// ./utils/custom.js
import { createTaskCollector, getCurrentSuite, setFn } from 'vitest/suite'

export { describe, beforeAll, afterAll } from 'vitest'

// this function will be called during collection phase:
// don't call function handler here, add it to suite tasks
// with "getCurrentSuite().task()" method
// note: createTaskCollector provides support for "todo"/"each"/...
export const myCustomTask = createTaskCollector(
  function (name, fn, timeout) {
    getCurrentSuite().task(name, {
      ...this, // so "todo"/"skip"/... is tracked correctly
      meta: {
        customPropertyToDifferentiateTask: true
      },
      handler: fn,
      timeout,
    })
  }
)
js
// ./garden/tasks.test.js
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()
  })
})
bash
vitest ./garden/tasks.test.js

WARNING

If you don't have a custom runner or didn't define runTest method, Vitest will try to retrieve a task automatically. If you didn't add a function with setFn, it will fail.

Released under the MIT License.