Write Tests
Overview
Write tests that prove behavior with the smallest useful fixture surface. Use Remix's own test/assert packages and describe/it style by default, keep package dependency graphs clean, and validate with the narrowest reliable commands.
Workflow
- Read the nearest
package.json, tsconfig.json, and existing sibling tests before choosing a runner or fixture style.
- Identify whether the package can depend on
@remix-run/test, or whether it is a dependency of @remix-run/test and must avoid a circular dependency.
- Keep the test close to the behavior owner. Prefer local helpers and direct Web/Node primitives over importing higher-level workspace packages as fixtures.
- Put test-only workspace packages in
devDependencies with workspace:^; do not add them to runtime dependencies.
- Run the package test and typecheck commands. Refresh
pnpm-lock.yaml when package metadata changes.
Runner Choice
- Write new and changed tests in
describe/it style. When touching a file that uses top-level test(), convert the affected tests to describe/it and leave unrelated tests alone.
- Use
@remix-run/test and @remix-run/assert by default for package tests.
- Use
node:test and node:assert/strict only when testing a package that is a dependency of @remix-run/test, or when adding Remix test/assert as a dependency would create a circular dependency.
- Do not keep a
test:bun script for node:test packages unless it has been validated. Bun's test runner does not automatically discover tests written with node:test imports.
Node test package script:
"test": "node --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'"
Node test imports:
import * as assert from 'node:assert/strict'
import { describe, it } from 'node:test'
Remix test package script:
"test": "remix test"
Remix test imports:
import * as assert from '@remix-run/assert'
import { describe, it } from '@remix-run/test'
Test Structure
- Name
describe() blocks after the public API or behavior owner, and name it() tests by observable behavior.
- Do not generate tests inside
describe() with loops or conditionals; this breaks per-test IDE execution.
- Prefer a few explicit cases over dense table tests when the cases document distinct behavior.
- Keep async tests awaited all the way through. Avoid resolving promises before the behavior under test has completed.
- Use mocks sparingly and locally. Prefer the Remix test context mocks when using
@remix-run/test; use mock.method()/mock.fn() from node:test only in node-runner exception packages.
Fixtures
- Keep fixtures minimal and local to the test file unless they are reused across multiple files for the same behavior surface.
- Avoid importing higher-level workspace packages just to build a fixture. For example, a fetch-handler test can branch on
new URL(request.url).pathname instead of depending on @remix-run/fetch-router.
- Prefer Web APIs and standards-aligned primitives when they express the fixture clearly.
- For e2e tests, serve the smallest app or handler that exercises the user-observable behavior under test.
- For HMR tests, use real file changes, child processes, watchers, and browser assertions when the public behavior depends on runtime coordination.
- Close spawned processes, asset servers, watchers, and HTTP servers in test cleanup.
Assertions
- Use
@remix-run/assert by default.
- Use
node:assert/strict only in node-runner exception packages that cannot depend on @remix-run/assert/@remix-run/test.
- Assert public behavior and observable side effects. Avoid asserting private implementation structure unless the package's public contract is the structure.
- For error tests, assert the error shape/message that consumers can rely on.
Dependency Hygiene
- Runtime code imports belong in
dependencies.
- Test files, fixtures, and runner-only imports belong in
devDependencies.
- Use
workspace:^ for workspace package dependencies unless the repo has an established reason for workspace:*.
- After changing package dependencies or scripts, run
pnpm i --lockfile-only --ignore-scripts and then a frozen install check.
- Reassess workspace cycles when changing testing infrastructure:
pnpm i --frozen-lockfile --ignore-scripts should not emit cyclic workspace dependency warnings.
Validation
Use the narrowest meaningful commands:
pnpm --filter @remix-run/<package> run test
pnpm --filter @remix-run/<package> run typecheck
pnpm i --frozen-lockfile --ignore-scripts
For cross-package or shared test infrastructure changes, also consider:
pnpm run test:changed
pnpm run typecheck:changed
pnpm run lint
1---2name: write-tests3description: Write, refactor, or review tests in the Remix repository. Use when adding or changing `.test.ts`/`.test.tsx` files, package test scripts, test fixtures, mocks, coverage tests, e2e tests, or package metadata for test-only dependencies.4---56# Write Tests78## Overview910Write tests that prove behavior with the smallest useful fixture surface. Use Remix's own test/assert packages and `describe`/`it` style by default, keep package dependency graphs clean, and validate with the narrowest reliable commands.1112## Workflow13141. Read the nearest `package.json`, `tsconfig.json`, and existing sibling tests before choosing a runner or fixture style.152. Identify whether the package can depend on `@remix-run/test`, or whether it is a dependency of `@remix-run/test` and must avoid a circular dependency.163. Keep the test close to the behavior owner. Prefer local helpers and direct Web/Node primitives over importing higher-level workspace packages as fixtures.174. Put test-only workspace packages in `devDependencies` with `workspace:^`; do not add them to runtime `dependencies`.185. Run the package test and typecheck commands. Refresh `pnpm-lock.yaml` when package metadata changes.1920## Runner Choice2122- Write new and changed tests in `describe`/`it` style. When touching a file that uses top-level `test()`, convert the affected tests to `describe`/`it` and leave unrelated tests alone.23- Use `@remix-run/test` and `@remix-run/assert` by default for package tests.24- Use `node:test` and `node:assert/strict` only when testing a package that is a dependency of `@remix-run/test`, or when adding Remix test/assert as a dependency would create a circular dependency.25- Do not keep a `test:bun` script for `node:test` packages unless it has been validated. Bun's test runner does not automatically discover tests written with `node:test` imports.2627Node test package script:2829```json30"test": "node --disable-warning=ExperimentalWarning --test './src/**/*.test.ts'"31```3233Node test imports:3435```ts36import * as assert from 'node:assert/strict'37import { describe, it } from 'node:test'38```3940Remix test package script:4142```json43"test": "remix test"44```4546Remix test imports:4748```ts49import * as assert from '@remix-run/assert'50import { describe, it } from '@remix-run/test'51```5253## Test Structure5455- Name `describe()` blocks after the public API or behavior owner, and name `it()` tests by observable behavior.56- Do not generate tests inside `describe()` with loops or conditionals; this breaks per-test IDE execution.57- Prefer a few explicit cases over dense table tests when the cases document distinct behavior.58- Keep async tests awaited all the way through. Avoid resolving promises before the behavior under test has completed.59- Use mocks sparingly and locally. Prefer the Remix test context mocks when using `@remix-run/test`; use `mock.method()`/`mock.fn()` from `node:test` only in node-runner exception packages.6061## Fixtures6263- Keep fixtures minimal and local to the test file unless they are reused across multiple files for the same behavior surface.64- Avoid importing higher-level workspace packages just to build a fixture. For example, a fetch-handler test can branch on `new URL(request.url).pathname` instead of depending on `@remix-run/fetch-router`.65- Prefer Web APIs and standards-aligned primitives when they express the fixture clearly.66- For e2e tests, serve the smallest app or handler that exercises the user-observable behavior under test.67- For HMR tests, use real file changes, child processes, watchers, and browser assertions when the public behavior depends on runtime coordination.68- Close spawned processes, asset servers, watchers, and HTTP servers in test cleanup.6970## Assertions7172- Use `@remix-run/assert` by default.73- Use `node:assert/strict` only in node-runner exception packages that cannot depend on `@remix-run/assert`/`@remix-run/test`.74- Assert public behavior and observable side effects. Avoid asserting private implementation structure unless the package's public contract is the structure.75- For error tests, assert the error shape/message that consumers can rely on.7677## Dependency Hygiene7879- Runtime code imports belong in `dependencies`.80- Test files, fixtures, and runner-only imports belong in `devDependencies`.81- Use `workspace:^` for workspace package dependencies unless the repo has an established reason for `workspace:*`.82- After changing package dependencies or scripts, run `pnpm i --lockfile-only --ignore-scripts` and then a frozen install check.83- Reassess workspace cycles when changing testing infrastructure: `pnpm i --frozen-lockfile --ignore-scripts` should not emit cyclic workspace dependency warnings.8485## Validation8687Use the narrowest meaningful commands:8889```sh90pnpm --filter @remix-run/<package> run test91pnpm --filter @remix-run/<package> run typecheck92pnpm i --frozen-lockfile --ignore-scripts93```9495For cross-package or shared test infrastructure changes, also consider:9697```sh98pnpm run test:changed99pnpm run typecheck:changed100pnpm run lint101```