---
title: 'Testing'
slug: testing
description: 'Unit-test with bun:test and run full-app tests in isolated processes with the runTests helper.'
---
## Testing
Testing support is **experimental**. The `bun test` runner works well for plain unit tests today, but the full-app helper below is new and its API may change.
### Unit tests
Pure functions, stores, and any non-server logic test directly with [`bun test`](https://bun.sh/docs/cli/test) — no Mochi-specific setup:
```ts
// src/lib/slugify.test.ts
import { expect, test } from 'bun:test';
import { slugify } from './slugify';
test('lowercases and dasherizes', () => {
expect(slugify('Hello World')).toBe('hello-world');
});
```
```sh
bun test
```
### Full-app tests
Tests that boot a real server with `Mochi.serve()` — to fetch a page, hit an API route, or assert on rendered HTML — must run **one file per process**. `Mochi.serve()` allows only one instance per process (it pins config on `globalThis.__mochi_config__`), so two server-booting test files in the same `bun test` run throw `Mochi.serve() has already been called.`
`runTests` solves this: it globs `src/**/*.test.ts` and runs each file in its own `bun test` process, parallelised across CPU cores. Add a small script to your package:
```ts
// scripts/run-tests.ts
#!/usr/bin/env bun
import { runTests } from 'mochi-framework';
await runTests();
```
Point your `test` script at it:
```json
// package.json
{
"scripts": {
"test": "bun scripts/run-tests.ts"
}
}
```
```sh
bun run test
```
Each file gets a fresh process, so every test can call `Mochi.serve({ port: 0 })` without colliding:
```ts
// src/routes.test.ts
import { afterAll, beforeAll, expect, test } from 'bun:test';
import type { Server } from 'bun';
import { Mochi } from 'mochi-framework';
import { routes } from './routes';
let server: Server;
beforeAll(async () => {
server = await Mochi.serve({ port: 0, logger: { enabled: false }, routes });
});
afterAll(() => server.stop(true));
test('GET / renders', async () => {
const res = await fetch(`http://localhost:${server.port}/`);
expect(res.status).toBe(200);
});
```
#### Options
`runTests(options?)` accepts:
- **`dir`** — package root to scan and run tests from. Defaults to the current working directory.
- **`sequential`** — files (relative to `dir`) that must run on their own, after the parallel batch — for tests that can't share machine state with others:
```ts
await runTests({ sequential: ['src/liveReload.test.ts'] });
```
`runTests` exits the process with code `1` if any file fails, so it drops straight into CI.