Skip to main content
Testing Remix applications is straightforward thanks to the web standards-based API. Since Remix uses the standard fetch() API, you can test your routes without any special test harness.

Testing Philosophy

Remix follows these testing principles:
  • Tests run from source - No build step required
  • Use standard APIs - Just use fetch() to test routes
  • Runtime-agnostic - Tests work the same across all runtimes
  • Fast feedback - Unit tests run in milliseconds

Unit Testing Routes

Test routes by calling router.fetch() with standard Request objects:
router.test.ts
import { describe, it } from 'node:test'
import * as assert from 'node:assert/strict'
import { createRouter } from 'remix/fetch-router'
import { route } from 'remix/fetch-router/routes'

let routes = route({
  users: '/users',
  user: '/users/:id',
})

let router = createRouter()

router.map(routes, {
  actions: {
    users() {
      return Response.json({ users: [{ id: '1', name: 'Alice' }] })
    },
    user({ params }) {
      return Response.json({ id: params.id, name: 'Alice' })
    },
  },
})

describe('Users API', () => {
  it('lists users', async () => {
    let response = await router.fetch('http://api.example.com/users')
    
    assert.equal(response.status, 200)
    let data = await response.json()
    assert.equal(data.users.length, 1)
    assert.equal(data.users[0].name, 'Alice')
  })

  it('gets user by id', async () => {
    let response = await router.fetch('http://api.example.com/users/1')
    
    assert.equal(response.status, 200)
    let data = await response.json()
    assert.equal(data.id, '1')
    assert.equal(data.name, 'Alice')
  })
})

Testing with Different Methods

Test POST, PUT, DELETE requests:
it('creates a user', async () => {
  let response = await router.fetch('http://api.example.com/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Bob', email: '[email protected]' }),
  })

  assert.equal(response.status, 201)
  let data = await response.json()
  assert.equal(data.name, 'Bob')
})

it('updates a user', async () => {
  let response = await router.fetch('http://api.example.com/users/1', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Alice Updated' }),
  })

  assert.equal(response.status, 200)
})

it('deletes a user', async () => {
  let response = await router.fetch('http://api.example.com/users/1', {
    method: 'DELETE',
  })

  assert.equal(response.status, 204)
})

Testing Middleware

Test middleware in isolation:
middleware.test.ts
import { describe, it } from 'node:test'
import * as assert from 'node:assert/strict'
import type { Middleware } from 'remix/fetch-router'

function auth(): Middleware {
  return (context, next) => {
    let token = context.headers.get('Authorization')
    if (!token || token !== 'Bearer secret') {
      return new Response('Unauthorized', { status: 401 })
    }
    return next()
  }
}

describe('Auth middleware', () => {
  it('rejects requests without token', async () => {
    let middleware = auth()
    let response = await middleware(
      { headers: new Headers() } as any,
      async () => new Response('OK')
    )

    assert.equal(response.status, 401)
  })

  it('allows requests with valid token', async () => {
    let middleware = auth()
    let headers = new Headers({ Authorization: 'Bearer secret' })
    let response = await middleware(
      { headers } as any,
      async () => new Response('OK')
    )

    assert.equal(response.status, 200)
  })
})

Testing Components

Test Remix components with the flush() method:
counter.test.ts
import { describe, it } from 'node:test'
import * as assert from 'node:assert/strict'
import { createRoot } from 'remix/component'
import type { Handle } from 'remix/component'

function Counter(handle: Handle) {
  let count = 0

  return () => (
    <button
      on={{
        click() {
          count++
          handle.update()
        },
      }}
    >
      Count: {count}
    </button>
  )
}

describe('Counter component', () => {
  it('increments on click', () => {
    let container = document.createElement('div')
    let root = createRoot(container)

    root.render(<Counter />)
    root.flush()

    let button = container.querySelector('button')
    assert.equal(button?.textContent, 'Count: 0')

    button?.click()
    root.flush()

    assert.equal(button?.textContent, 'Count: 1')
  })
})

Testing Database Queries

Use in-memory SQLite for fast database tests:
import { Database } from 'better-sqlite3'
import { createDatabase, table, column as c } from 'remix/data-table'
import { createSqliteDatabaseAdapter } from 'remix/data-table-sqlite'

describe('User queries', () => {
  it('creates and finds users', async () => {
    // Create in-memory database
    let sqlite = new Database(':memory:')
    let db = createDatabase(createSqliteDatabaseAdapter(sqlite))

    let users = table({
      name: 'users',
      columns: {
        id: c.integer(),
        name: c.varchar(255),
        email: c.varchar(255),
      },
    })

    // Create table
    sqlite.exec(`
      CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        name TEXT,
        email TEXT
      )
    `)

    // Test create
    await db.create(users, {
      name: 'Alice',
      email: '[email protected]',
    })

    // Test find
    let user = await db.find(users, { email: '[email protected]' })
    assert.equal(user?.name, 'Alice')
  })
})

Integration Testing

Test full request/response cycles:
import { createServer } from 'remix/node-fetch-server'

describe('Full application', () => {
  it('handles complete request', async () => {
    let server = createServer(router)
    
    // Start server on random port
    await new Promise<void>((resolve) => {
      server.listen(0, () => resolve())
    })

    let address = server.address()
    let port = typeof address === 'object' ? address?.port : 3000

    // Make real HTTP request
    let response = await fetch(`http://localhost:${port}/users`)
    assert.equal(response.status, 200)

    server.close()
  })
})

Test Utilities

Create helper utilities for common test scenarios:
test-utils.ts
export function createTestRequest(
  url: string,
  options?: RequestInit
): Request {
  return new Request(url, options)
}

export function createFormRequest(
  url: string,
  data: Record<string, string>
): Request {
  let formData = new FormData()
  for (let [key, value] of Object.entries(data)) {
    formData.append(key, value)
  }

  return new Request(url, {
    method: 'POST',
    body: formData,
  })
}

export async function expectJson(response: Response) {
  assert.equal(response.headers.get('Content-Type'), 'application/json')
  return await response.json()
}

Running Tests

Node.js

node --test

Bun

bun test

Deno

deno test

Best Practices

  • Test behavior, not implementation
  • Use in-memory databases for fast tests
  • Test error cases and edge cases
  • Keep tests focused and isolated
  • Use descriptive test names
  • Mock external services

Component Testing

Component API with flush() for testing

Data Table

Testing database queries

Build docs developers (and LLMs) love