# Testing ## What This Covers How to test the two layers most Remix code lives in: HTTP behavior and DOM behavior. Read this when the task involves: - Driving the router with `router.fetch(new Request(...))` and asserting on the returned `Response` - Building a fresh router per test for session, storage, or database isolation - Rendering components into a real DOM with `render(...)` or `createRoot(...)` - Configuring `remix test` discovery, excludes, and coverage - Using adjacent CLI checks such as `remix routes`, `remix doctor`, and `remix version` - Choosing which layer to test for a given behavior For session and auth test setup, see `auth-and-sessions.md`. For component lifecycle, see `component-model.md`. ## Two Shapes Remix tests run with `remix test`, use `remix/test` for the test framework, and use `remix/assert` for assertions. Two main shapes: - **Server / router tests** — drive the router with `router.fetch(new Request(...))` and assert on the returned `Response`. No DOM, no browser harness. - **Component tests** — render a component into a real DOM `Element` with `render(...)`, or use `createRoot(...)` directly when you need lower-level root control. ## Server / Router Tests Treat the router as a pure `(Request) => Promise` function. Build a fresh app router per test (or per suite) so middleware state — sessions, in-memory storage, the database — stays isolated. ```ts import * as assert from 'remix/assert' import { describe, it } from 'remix/test' import { createBookstoreRouter } from '../app/router.ts' import { routes } from '../app/routes.ts' describe('home', () => { it('responds 200 with the home page', async () => { let router = createBookstoreRouter() let response = await router.fetch(new Request('http://localhost' + routes.home.href())) assert.equal(response.status, 200) assert.match(await response.text(), /Welcome to the Bookstore/) }) }) ``` Use `routes..href(...)` to build URLs in tests so they stay in sync with the route definition. For form-style POSTs, attach a `FormData` body to the `Request`. For tests that need a known session, swap in `createMemorySessionStorage()` and a test cookie when constructing the router. ```ts import { createMemorySessionStorage } from 'remix/session-storage/memory' import { createCookie } from 'remix/cookie' let router = createBookstoreRouter({ sessionCookie: createCookie('session', { secrets: ['test'] }), sessionStorage: createMemorySessionStorage(), }) ``` Use `createTestServer` from `remix/node-fetch-server/test` when the behavior depends on a real HTTP origin, redirects, streaming, cookies through a network boundary, or browser-style `fetch`: ```ts import { createTestServer } from 'remix/node-fetch-server/test' let server = await createTestServer((request) => router.fetch(request)) try { let response = await fetch(new URL(routes.home.href(), server.baseUrl)) assert.equal(response.status, 200) } finally { await server.close() } ``` ## Test Runner Config Configure discovery and coverage in `remix-test.config.ts` or with CLI flags: ```ts export default { glob: { test: '**/*.test{,.e2e}.{ts,tsx}', e2e: '**/*.test.e2e.{ts,tsx}', exclude: 'node_modules/**', }, coverage: { dir: '.coverage', include: ['app/**/*.{ts,tsx}'], exclude: ['app/**/*.test.{ts,tsx}'], statements: 80, lines: 80, branches: 70, functions: 80, }, } ``` Use `remix test --coverage` to enable coverage with defaults. Use `glob.exclude` when discovery would otherwise enter generated output, symlinked workspaces, or other paths that should not produce tests. ## Component Tests Use `render(...)` from `remix/ui/test` for most component tests. It creates a real DOM container, flushes the initial render, and returns `act(...)` so interactions can flush pending updates before assertions. Use `createRoot(container)` from `remix/ui` directly when a test needs explicit control over root rendering, flushing, or disposal. ### Basic pattern ```tsx import * as assert from 'remix/assert' import { render } from 'remix/ui/test' let result = render() let button = result.$('button')! await result.act(() => button.click()) assert.match(result.container.textContent ?? '', /1/) result.cleanup() ``` ### Why act / flush - **After initial render** — ensures event listeners are attached and the DOM is ready for interaction. - **After interactions** — applies updates from `handle.update()` calls triggered by events. - **After async work resolves** — applies updates from resolved `queueTask(...)` callbacks. ### Async operations For components with async operations in `queueTask`, use `act(...)` after each async step: ```tsx let result = render() assert.equal(result.container.textContent, 'Loading...') await waitForFetch() await result.act(() => {}) assert.equal(result.container.textContent, 'Expected data') ``` ### Component removal Use `result.cleanup()` or `root.dispose()` to remove the component tree and verify cleanup behavior: ```tsx let result = render() assert.ok(result.$('.content')) result.cleanup() assert.throws(() => result.$('.content'), /cleaned up/) ``` ### Guidelines - Prefer real DOM interactions over mocking framework behavior. - Avoid testing implementation-only markers unless they are the only stable synchronization point. - One representative flow proving a behavior is better than repeating the same assertion across many paths.