Skip to content

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 ​

ts
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.

ts
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.

ts
it('adds two numbers', (t) => {
  t.assert.equal(1 + 1, 2)
})

Async tests are supported by returning a promise:

ts
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:

PropertyDescription
t.assertAssertion object with equal, deepEqual, ok, throws, etc.
t.mockCreate mock functions.
t.spyOnSpy 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.

ts
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.

ts
after(async () => {
  await db.close()
})

beforeEach(fn) ​

Runs before each test in the current describe block.

ts
beforeEach(async () => {
  await db.truncate('users')
})

afterEach(fn) ​

Runs after each test in the current describe block.

ts
afterEach(() => {
  cleanup()
})

Assertions ​

The t.assert object provides assertion methods compatible with the assert package:

ts
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.

ts
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:

ts
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 ​

PropertyTypeDescription
callsCall[]Array of recorded calls. Each has arguments and result.
calls.lengthnumberNumber of times the mock was called.
reset()() => voidClears recorded calls.

t.spyOn(object, method) ​

Creates a spy on an existing method. The original method still executes, but calls are recorded.

ts
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:

ts
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:

ts
// 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:

bash
# 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 dot

CLI Options ​

OptionDescription
--watchRe-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.
--bailStop after the first test failure.

Examples ​

Testing a Route Handler ​

ts
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 ​

ts
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 ​

ts
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')
  })
})
  • assert --- Cross-runtime assertion utilities.
  • Testing Guide --- End-to-end guide to testing Remix applications.

Released under the MIT License.