Testing
Testing is a first-class feature of Server.js. Call .test() on your app to get a lightweight test client that runs requests through the full middleware stack without starting an HTTP server.
Basic setup
Keep your server in its own file so tests can import it:
// src/index.js
import server from '@server/next';
export default server()
.get('/', () => 'Hello world')
.get('/users', () => User.list())
.post('/users', (ctx) => User.create(ctx.body));// src/index.test.js
import app from './index.js';
const api = app.test();
it('returns hello world', async () => {
const res = await api.get('/');
expect(await res.text()).toBe('Hello world');
});Available methods
The test client mirrors the HTTP methods. Each call returns a standard Response:
const api = app.test();
api.get('/path', options?)
api.post('/path', body?, options?)
api.put('/path', body?, options?)
api.patch('/path', body?, options?)
api.delete('/path', options?)
api.head('/path', options?)
api.options('/path', options?)body: the request body. Accepts a string, plain object (serialized as JSON),FormData, orReadableStream.options: standardRequestInitoptions (e.g.headers).
Reading the response
Each method returns a standard web Response:
const res = await api.get('/users');
res.status // 200
res.headers.get('content-type') // 'application/json'
await res.text() // raw body as string
await res.json() // parsed JSON bodySending a body
Pass a plain object and Server.js will serialize it as JSON and set Content-Type: application/json automatically:
const res = await api.post('/users', {
name: 'Francisco',
email: '[email protected]',
});
expect(res.status).toBe(201);
expect(await res.json()).toMatchObject({
name: 'Francisco',
});For other body types:
// Plain text
await api.post('/echo', 'Hello world');
// FormData (e.g. file uploads)
const form = new FormData();
form.append('name', 'Francisco');
await api.post('/upload', form);
// Custom headers
await api.get('/secure', {
headers: { authorization: 'Bearer token123' },
});Full example
// src/books.test.js
import app from './index.js';
const api = app.test();
describe('books API', () => {
it('lists books', async () => {
const res = await api.get('/books');
expect(res.status).toBe(200);
expect(await res.json()).toEqual([]);
});
it('creates a book', async () => {
const res = await api.post('/books', {
title: 'Dune',
author: 'Herbert',
});
expect(res.status).toBe(201);
const body = await res.json();
expect(body.title).toBe('Dune');
});
it('returns 404 for unknown books', async () => {
const res = await api.get('/books/9999');
expect(res.status).toBe(404);
});
it('deletes a book', async () => {
const res = await api.delete('/books/1');
expect(res.status).toBe(204);
});
});Testing signed-in requests
A function is a whole auth integration, so the simplest signed-in test swaps the shape rather than minting a credential:
import server from '@server/next';
const rows = new Map([['u1', { id: 'u1', email: '[email protected]', role: 'admin' }]]);
const auth = process.env.NODE_ENV === 'test'
? (ctx) => rows.get(ctx.headers['x-test-user'])
: { providers: 'github', onLogin, getUser };
const api = server({ auth })
.get('/me', (ctx) => ctx.user || 401)
.test();
it('resolves the signed-in user', async () => {
const res = await api.get('/me', { headers: { 'x-test-user': 'u1' } });
expect((await res.json()).email).toBe('[email protected]');
});Because every shape ends in the same ctx.user, the handlers under test cannot tell the difference, and the test stays about your routes rather than about credentials.
To exercise the real credential instead, drive the login routes: GET /auth/login/<provider> and the callback go through the provider, so that path is an integration test against a real (or stubbed) provider rather than a unit test.
Limitations
The test client simulates requests in-process; it does not open a real TCP connection. This means:
- Compression (
brotli,gzip) andcontent-encodingheaders are not applied (those are usually set by edge proxies). - Edge-only features like
cf-*headers won't be present.
For integration tests that require a real HTTP connection, start the server on a test port and use fetch directly.