createTestServer
Start the real GioJS server for your app on a free port, with a private cache, for end-to-end tests of everything the Rust layer does.
ts
import { createTestServer } from '@gio.js/core/testing';
const server = await createTestServer();
const res = await fetch(`${server.url}/posts/1`);
await server.close();Reference
| Option | Type | Default | Description |
|---|---|---|---|
appDir | string | GIO_APP_DIR, else ./app | The app/ directory; the server runs from its parent, the project root. |
env | Record<string, string | undefined> | {} | Extra environment for the server and its worker; undefined removes an inherited variable. { NODE_ENV: 'development' } gives a development server. |
port | number | a free port | The port on 127.0.0.1. With a fixed port there is no retry when it is taken. |
binary | string | GIO_SERVER_BIN, else @gio.js/server | The giojs-server binary to run. Without it, GIO_SERVER_BIN, then the platform binary that @gio.js/server installed, then (inside the GioJS repository) a target/debug or target/release build. |
timeoutMs | number | 60000 | How long to wait for the worker to be ready, in milliseconds. |
Returns
A Promise<TestServer>, resolved once the Node worker is ready:
| Field | Type | Default | Description |
|---|---|---|---|
url | string | - | The base URL, http://127.0.0.1:<port>, without a trailing slash. |
port | number | - | The port it listens on. |
logs() | string | - | Everything the server and its worker printed so far (the last 1 MiB). |
close() | Promise<void> | - | Stops the server and every process it started; safe to call twice. |
Behavior
- The server reads your
gio.toml,middleware.ts,gio.config.tsand.envfiles as in production; only the listen address is overridden, throughGIO_HOSTandGIO_PORT. - Each server gets its own temporary page cache, image cache and IPC sockets, so several can run side by side, and the project's
.gio/cacheis never read or written. They are deleted onclose(). - It polls
/_gio/healthuntil the worker reports ready (a404there -[health] enabled = false- counts as ready, since the port opens only once the worker is up). - Values the test kit loaded from
.envfiles and the random session secret it may have set are not passed on: the server loads the files itself, by its own mode. Variables your test set are passed on. - A running server never keeps the test process alive. When the process exits, or crashes, exit hooks kill the servers; when no hook can run (a
SIGKILL, a vitest thread torn down), a small watchdog process does.
Errors
- No binary found: it throws, naming
npm install --save-dev @gio.js/serverandGIO_SERVER_BIN. AbinaryorGIO_SERVER_BINpath that does not exist throws too. - The server exits before it is ready: it throws with the server's own error first - a
gio.tomlrefusal, or a worker that could not boot, such as amiddleware.tsthat throws (giojs-server exited before it was ready: the Node worker exited before it was ready (exit status: 1): .../middleware.ts failed to load: ...) - then the last 40 lines of its log. - Not ready within
timeoutMs: it stops the server and throws, with the log. - The chosen free port was taken in the meantime: it retries on another port, up to three times.
Examples
Guards, CSRF and headers (vitest)
For a project with a session guard on /admin and the /api/notes handler from Request body errors:
gio.toml
[[guards]]
path = "/admin/*rest"
require_session = true
redirect_to = "/login"tests/server.test.ts
import { afterAll, beforeAll, expect, it } from 'vitest';
import { createTestServer, type TestServer } from '@gio.js/core/testing';
let server: TestServer;
beforeAll(async () => {
server = await createTestServer({ env: { GIO_SESSION_SECRET: 'test-only-secret-at-least-32-bytes-long' } });
}, 60_000); // the worker builds client bundles before it is ready
afterAll(() => server.close());
it('guards /admin in Rust', async () => {
const res = await fetch(`${server.url}/admin`, { redirect: 'manual' });
expect(res.status).toBe(302);
expect(res.headers.get('location')).toBe('/login');
});
it('refuses a cross-site POST', async () => {
const res = await fetch(`${server.url}/api/notes`, {
method: 'POST',
headers: { origin: 'https://evil.example', 'content-type': 'application/json' },
body: '{"text":"hi"}',
});
expect(res.status).toBe(403);
});
it('sends the default security headers', async () => {
const res = await fetch(server.url);
expect(res.headers.get('x-content-type-options')).toBe('nosniff');
});The page cache and revalidation
With the cached /posts/[id] page and the PUT handler that calls revalidateTag from revalidateTag:
ts
it('purges a post on update', async () => {
await fetch(`${server.url}/posts/1`); // miss; stored
const cached = await fetch(`${server.url}/posts/1`);
expect(cached.headers.get('x-gio-cache')).toMatch(/^hit/);
await fetch(`${server.url}/api/posts/1`, {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ title: 'Changed' }),
});
const fresh = await fetch(`${server.url}/posts/1`);
expect(fresh.headers.get('x-gio-cache')).toMatch(/^miss/);
expect(await fresh.text()).toContain('Changed');
});fetch() in Node sends neither Origin nor Sec-Fetch-Site, so its POSTs pass the CSRF check like any client that is not a browser page. Set an origin header of another site to test the refusal, as in the first example.
Good to know
- Startup takes seconds - the worker builds the client bundles first. Start one server per test file in
beforeAll, give that hook a longer timeout, and close it inafterAll. - Production mode unless you pass
NODE_ENV=developmentinenv, so the server needs what production needs - for example aGIO_SESSION_SECRETwhen a session storage is created at import. - Plain HTTP only. A
gio.tomlwith[server.tls]enabled needs a test copy of the project without it. - The binary comes from the
@gio.js/serverpackage (scaffolded apps have it), or fromGIO_SERVER_BIN- useful in CI with a binary you built.
Related
- Testing: The real server
- renderPage and callRoute - in-process, much faster
- Security: CSRF protection
- Environment variables -
GIO_SERVER_BIN,GIO_HOST,GIO_PORT
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Introduced. |