Vitest reporter with OpenTelemetry support for auto-instrumentation with Dagger.
Requires Dagger v1.0.0-beta.15 or later.
dagger install github.com/dagger/vitest
# run every Vitest project's tests (no additional setup needed)
dagger checkProjects, their test files and each file's tests are collections. dagger check lists one check per test file, addressed as
vitest/projects/test-files/tests/test, and runs each file in its own
vitest invocation:
# list what would run, one line per test file
dagger check -l --all
# every project's tests
dagger check --vitest
# the test check by name (the same thing, alongside other modules' test checks)
dagger check --check test
# one project
dagger check --vitest-project=apps/web
# some test files of one project
dagger check vitest/projects/test-files/tests/test \
--vitest-project=apps/web \
--vitest-test-file=src/a.test.ts --vitest-test-file=src/b.test.ts
# a single test, by the full name `vitest list` prints after the file path
dagger check --vitest-test='the sum > adds two numbers'
# the keys of each collection
dagger list vitest-projects -a
dagger list vitest-test-files -a --vitest-project=apps/web
dagger list vitest-tests -a --vitest-test-file=src/a.test.ts| Flag | Selects |
|---|---|
--vitest |
every check of this module |
--check test |
checks named test, in every installed module |
--vitest-project=PATH |
a project, by workspace-root-relative root (repeatable) |
--vitest-projects |
every project |
--vitest-test-file=PATH |
a test file, by project-relative path (repeatable) |
--vitest-test-files |
every test file |
--vitest-test=NAME |
a test, by full name (repeatable) |
--vitest-tests |
every test |
The key flags are named after the item types, VitestProject,
VitestTestFile and VitestTest; dagger check --help lists the flags in
effect.
When none of a file's tests is selected out, the whole file runs, so tests the
listing did not report still run. Otherwise only the selected tests run,
through Vitest's --testNamePattern: their full names are escaped and
anchored, so a name holding regexp characters matches only itself.
Test files are passed to Vitest by path, and because Vitest matches file
arguments as substrings (so src/a.test.ts would also run src/a.test.tsx or
lib/src/a.test.ts), every other discovered test file with the same file name
is removed from the container first. This holds whatever include/exclude
the Vitest config or its inline projects set.
A project's test files can also be run together, in one vitest invocation,
through testFiles(ws) — see below. With
nothing selected out that is a plain vitest, so Vitest's own config decides
what runs; dagger check does not take that path, so a test file that only
the config's include adds is not checked.
If the Vitest config defines projects you don't want to run in a container,
such as browser or e2e projects, name the ones to run with Vitest's --project
flag:
[modules.vitest.settings]
flags = ["--project", "unit*"]Discovery is anchored at the directory you run Dagger from, so cd selects
projects without any flag:
- at a project root, that project and the projects below it;
- inside a project's subdirectory, that project, plus any project below the directory;
- in a directory that belongs to no project, the projects below it.
# a monorepo holding apps/web and apps/admin
cd apps/web/src && dagger check # runs apps/web only
cd apps && dagger list vitest-projects -a # -> apps/admin, apps/webProject keys are workspace-root-relative whatever the directory, . for a
project at the root.
A project is any directory holding a vitest.config.* or vite.config.*
file, found with Workspace.findRoots (node_modules excluded). A project
inside another one is a project of its own. Note that a whole run of the outer
project is a plain vitest there, which also picks up the inner project's
files unless the outer config excludes them.
Test files are keyed by their path relative to the project root. They are
found by one ripgrep search per project with Vitest's default include,
**/*.{test,spec}.?(c|m)[jt]s?(x), pruning node_modules, .git and the
subtrees of nested projects. Listing test files runs no container. Static
discovery does not read your Vitest config, so:
- A custom
includeorexcludein the config is not seen. Files your config adds still run whenever the whole project runs (no test file selected out), but cannot be selected on their own; files your config excludes are listed and, when selected alone, make Vitest report that it found no test files. - A project where no file matches the default pattern has no test files to
check, so
dagger checkdoes not run it. Call the project'stestfunction from the API to run it. - Empty files are not listed.
Tests are keyed by the full name vitest list prints after the file path,
suites included, e.g. the sum > adds two numbers. Unlike test files these
come from Vitest itself — one vitest list <file> run per file, which needs
the project installed — because names can be computed and test.each
generates them. Nothing lists them until you ask: dagger check with no test
selected never runs vitest list. A config defining Vitest projects lists
each file once per project that includes it, under one set of keys.
test.skip and test.todo are left out of vitest list, so they are not
keys and cannot be selected on their own. They are reported as skipped when
their file runs.
vitest list arrives in Vitest 2.1. On an older Vitest it is read as a file
filter, so a project has no tests to select and list(ws) reports nothing;
select test files instead.
Each project is installed from its install root: the nearest enclosing
workspace root (a directory with pnpm-workspace.yaml, or a package.json
with "workspaces"), else the nearest directory with a lockfile, else the
nearest package.json. That directory is mounted, dependencies are installed
there, and Vitest runs with the project directory as its working directory, so
workspace: and catalog: dependencies resolve.
- The package manager comes from the install root's
package.json"packageManager"field, else its lockfile (pnpm-lock.yamlorpnpm-workspace.yaml,yarn.lock,bun.lock/bun.lockb, otherwise npm), unless thepackageManagersetting names one. pnpm and yarn run through corepack, which is installed when the image lacks it (asnode:25images do) and honours the field's version. - The install sees only the files it reads (every
package.json, lockfiles,pnpm-workspace.yaml,.npmrc,.yarnrc*,.yarn/{releases,plugins,patches},patches/, the directories offile:/link:/portal:dependencies and injected workspace packages, which the install copies, and the files packages name asbin, so their commands get linked); the rest of the source (minus.gitignored files) is added afterwards, so editing source does not re-run the install. npm, pnpm, yarn and bun caches andCOREPACK_HOMEare on cache volumes (the pnpm store is passed with--store-dir, which pnpm 12 requires). Apostinstallthat needs other source files fails at this step; passinstallFlags = ["--ignore-scripts"]if the tests don't need it. - Browsers: Puppeteer's Chrome download on install is left on (Vitest's
browser mode can use it) and its directory,
~/.cache/puppeteer, is on a cache volume, so it happens once. Playwright downloads nothing on install; for browser-mode tests with the Playwright provider, use a base image that ships its browsers (e.g.baseImageAddress = "mcr.microsoft.com/playwright:<version>") or install them in yourbuildscript;~/.cache/ms-playwrightis on a cache volume too, so that download also happens once. Cypress's binary download and git hook installers (husky, simple-git-hooks) are switched off. - Vitest runs from the project's own
node_modules/.bin/vitest(looked up from the project to the install root, or through yarn under Plug'n'Play). A project with apackage.jsonbut no vitest installed fails with a message saying so; only a project with nopackage.jsonat all runsnpx vitest. - The OpenTelemetry reporter is loaded with
NODE_OPTIONS=--import; nothing is added to your dependencies. It loads what its exporter needs up front, so tests that delete globals such asReadableStreamorfetchdon't break the export.
A failure names the project and the step, with the end of its output, e.g.
Vitest project apps/web: install failed (pnpm install --store-dir /root/.pnpm-store, exit 1): ... or
Vitest project apps/web: vitest failed (exit 1): ....
Settings live in the workspace dagger.toml, or can be set with
dagger settings vitest <name> <value> and removed with
dagger settings -u vitest <name>:
[modules.vitest.settings]
# The package manager: npm, pnpm, yarn or bun. Default: "" (detect).
packageManager = "pnpm"
# Flags appended to the install command. Default: [].
installFlags = ["--ignore-scripts"]
# Environment variables for the install and the tests, as KEY=VALUE. Default: [].
environment = ["TZ=UTC", "NODE_ENV=test"]
# Run the package manager's build script before the tests. Default: false.
build = true
# Flags to pass to every vitest run, and to `list`. Default: [].
flags = ["--reporter=verbose"]
# The base image. Default: "node:25-alpine".
baseImageAddress = "node:25-alpine"
# Extra workspace files mounted at their paths (see below). Default: [].
includeExtraFiles = []projects(ws) returns the project collection. Collections expose keys,
get(key:), subset(keys:) and batch; each project's testFiles(ws) is
the test-file collection, and each test file's tests(ws) is its test
collection. A check called through a dependency comes back as a Check that
has not run yet, so wrap it:
let run(check: Check!): Void {
if (check.pass == false) {
raise check.error.message ?? "check failed"
}
null
}
testWeb(ws: Workspace!): Void @check {
let projects = vitest.projects(ws)
# every project, as a plain function
projects.batch.test(ws)
# one project's selected test files, in one vitest invocation
let files = projects.get(key: "apps/web").testFiles(ws)
files.subset(keys: ["src/a.test.ts"]).batch.test(ws)
# a single test file
let file = files.get(key: "src/a.test.ts")
file.test(ws)
# selected tests of that file, and a single test -- these are checks
let tests = file.tests(ws)
run(tests.subset(keys: ["the sum > adds two numbers"]).batch.test(ws))
run(tests.get(key: "the sum > adds two numbers").test(ws))
}
vitest.project(ws, path) looks a project up by its root, relative to the
workspace cwd. A project also has list(ws) (vitest list output, failing
when Vitest reports collection errors) and source(ws). From the CLI:
dagger call vitest project --path=apps/web listTo run tests, use dagger check (in CI too): dagger call on a check
function does not fail the command when the check fails.
Only the project's install root is mounted into the test container. When a
test reads a file that lives outside it — typically a fixture shared with code
elsewhere in a monorepo — list workspace-root patterns in includeExtraFiles.
They are mounted at their workspace-relative paths, so relative imports that
escape the install root resolve as they do on disk:
[modules.vitest.settings]
includeExtraFiles = ["testdata/**", "schema/*.json"]or dagger settings vitest includeExtraFiles '["testdata/**", "schema/*.json"]'.
Tests are run with access to the engine running them, so a test using a Dagger SDK reaches that engine rather than provisioning one of its own:
import { connection, dag } from "@dagger.io/dagger"
test("builds", async () => {
await connection(async () => {
expect(await dag.container().from("alpine").withExec(["echo", "hi"]).stdout()).toBe("hi\n")
})
})Each connection opens its own session rather than joining the one running
Vitest, and the workspace's .git is mounted, so a test sees the same workspace
it would on your machine. That is what lets a generated module client work:
import { dag } from "@dagger.io/my-module"
test("the module answers", async () => {
await connection(async () => {
expect(await dag.myModule().greeting("vitest")).toBe("Hello, vitest!")
})
})A client serves its module by workspace address before its first query, which
needs a workspace, which is found by walking up to a .git directory. Joining
Vitest's own session instead would resolve that address against this module
and fail.
Such tests are usually slower than the 5s Vitest allows by default, so raise
testTimeout in your Vitest config.
If you prefer to directly install the vitest library, run:
npm install --save-dev @dagger.io/vitestThen set the import in your NODE_OPTIONS when executing your tests:
NODE_OPTIONS="$NODE_OPTIONS --import @dagger.io/vitest/register" npx vitest runThat's it! The reporter will automatically create OpenTelemetry spans for:
- Test files (modules)
- Test suites (describe blocks)
- Individual tests (it/test blocks)
Test spans include dagger.io/ui.boundary plus OpenTelemetry test semantic convention attributes: test.case.name, test.case.result.status, and test.suite.name.
Suite spans include dagger.io/ui.boundary, test.suite.name, and test.suite.run.status.
Console output captured by Vitest is also emitted as OpenTelemetry log records associated with the test span, using stdio.stream to distinguish stdout and stderr.
test-file.ts (module span)
└─ describe block (suite span)
├─ test 1 (test span)
├─ SELECT * FROM users (inside test span)
└─ Container.withExec(...) (inside test span)
└─ test 2 (test span)
Apache-2.0