test
The test package provides a built-in test runner for Remix applications. It offers a familiar describe/it API, lifecycle hooks, mock functions, and spies. No additional test framework is needed.
Installation
The test package is included with Remix. No additional installation is needed.
Import
import {
describe,
it,
suite,
before,
after,
beforeEach,
afterEach,
beforeAll,
afterAll,
} from 'remix/test'API
describe(name, fn) / suite(name, fn)
Groups related tests. Can be nested to any depth. suite is an alias for describe.
describe('User', () => {
describe('validation', () => {
it('requires an email', (t) => {
// ...
})
})
})it(name, fn)
Defines a single test case. The callback receives a test context t with assertion and mocking utilities.
it('adds two numbers', (t) => {
t.assert.equal(1 + 1, 2)
})Async tests are supported by returning a promise:
it('fetches a user', async (t) => {
let user = await getUser('123')
t.assert.ok(user)
t.assert.equal(user.name, 'Alice')
})Test Context (t)
Every test callback receives a context object t with the following properties:
| Property | Description |
|---|---|
t.assert | Assertion object with equal, deepEqual, ok, throws, etc. |
t.mock | Create mock functions. |
t.spyOn | Spy on methods of an object. |
Lifecycle Hooks
Hooks run at specific points in the test lifecycle. They can be placed inside describe blocks or at the top level.
before(fn) / beforeAll(fn)
Runs once before all tests in the current describe block.
describe('database', () => {
before(async () => {
await db.migrate()
})
it('queries data', async (t) => {
// database is migrated
})
})after(fn) / afterAll(fn)
Runs once after all tests in the current describe block.
after(async () => {
await db.close()
})beforeEach(fn)
Runs before each test in the current describe block.
beforeEach(async () => {
await db.truncate('users')
})afterEach(fn)
Runs after each test in the current describe block.
afterEach(() => {
cleanup()
})Assertions
The t.assert object provides assertion methods compatible with the assert package:
it('assertions', (t) => {
t.assert.ok(true)
t.assert.equal(1 + 1, 2)
t.assert.notEqual(1, 2)
t.assert.strictEqual('a', 'a')
t.assert.deepEqual([1, 2], [1, 2])
t.assert.throws(() => { throw new Error() })
t.assert.match('hello', /ell/)
})Mocking
t.mock(fn?)
Creates a mock function that records its calls. Optionally wraps an existing function.
it('calls the handler', (t) => {
let handler = t.mock()
emitter.on('event', handler)
emitter.emit('event', 'data')
t.assert.equal(handler.calls.length, 1)
t.assert.deepEqual(handler.calls[0].arguments, ['data'])
})With an implementation:
let fetch = t.mock(async (url: string) => {
return new Response('mocked')
})
let response = await fetch('/api')
t.assert.equal(await response.text(), 'mocked')
t.assert.equal(fetch.calls.length, 1)Mock Properties
| Property | Type | Description |
|---|---|---|
calls | Call[] | Array of recorded calls. Each has arguments and result. |
calls.length | number | Number of times the mock was called. |
reset() | () => void | Clears recorded calls. |
t.spyOn(object, method)
Creates a spy on an existing method. The original method still executes, but calls are recorded.
it('logs requests', (t) => {
let spy = t.spyOn(console, 'log')
console.log('hello')
t.assert.equal(spy.calls.length, 1)
t.assert.deepEqual(spy.calls[0].arguments, ['hello'])
})Replace the implementation:
let spy = t.spyOn(Date, 'now')
spy.mockImplementation(() => 1704067200000)
t.assert.equal(Date.now(), 1704067200000)Spies are automatically restored after the test completes.
Configuration
Configure the test runner by creating a remix-test.config.ts file in your project root:
// remix-test.config.ts
import { defineConfig } from 'remix/test'
export default defineConfig({
// Glob patterns for test files
include: ['app/**/*.test.ts'],
// Glob patterns to exclude
exclude: ['node_modules/**'],
// Number of test files to run in parallel
concurrency: 4,
// Test timeout in milliseconds
timeout: 5000,
// Reporter: 'dot', 'spec', or 'json'
reporter: 'spec',
})CLI
Run tests from the command line:
# Run all tests
npx remix test
# Run specific files
npx remix test app/routes/user.test.ts
# Watch mode -- re-run on file changes
npx remix test --watch
# Set concurrency
npx remix test --concurrency 8
# Choose a reporter
npx remix test --reporter dotCLI Options
| Option | Description |
|---|---|
--watch | Re-run tests when files change. |
--concurrency <n> | Number of test files to run in parallel. |
--reporter <name> | Output format: spec, dot, or json. |
--timeout <ms> | Override the default test timeout. |
--bail | Stop after the first test failure. |
Examples
Testing a Route Handler
import { describe, it } from 'remix/test'
describe('GET /users/:id', () => {
it('returns a user', async (t) => {
let request = new Request('http://localhost/users/123')
let response = await handler(request)
t.assert.equal(response.status, 200)
let user = await response.json()
t.assert.equal(user.id, '123')
t.assert.ok(user.name)
})
it('returns 404 for unknown users', async (t) => {
let request = new Request('http://localhost/users/unknown')
let response = await handler(request)
t.assert.equal(response.status, 404)
})
})Mocking a Database
import { describe, it, beforeEach } from 'remix/test'
describe('createUser', () => {
beforeEach(async () => {
await db.truncate('users')
})
it('inserts a user', async (t) => {
let user = await createUser({ name: 'Alice', email: 'alice@example.com' })
t.assert.ok(user.id)
t.assert.equal(user.name, 'Alice')
})
it('rejects duplicate emails', async (t) => {
await createUser({ name: 'Alice', email: 'alice@example.com' })
await t.assert.rejects(
() => createUser({ name: 'Bob', email: 'alice@example.com' }),
/duplicate/,
)
})
})Spying on External Services
import { describe, it } from 'remix/test'
describe('sendWelcomeEmail', () => {
it('sends an email to the user', async (t) => {
let spy = t.spyOn(emailService, 'send')
spy.mockImplementation(async () => ({ success: true }))
await sendWelcomeEmail('alice@example.com')
t.assert.equal(spy.calls.length, 1)
t.assert.equal(spy.calls[0].arguments[0], 'alice@example.com')
})
})Related
- assert --- Cross-runtime assertion utilities.
- Testing Guide --- End-to-end guide to testing Remix applications.