🍡 mochi

SSR framework for Svelte 5 + Bun with islands-based selective hydration

On this page

Testing

Unit tests

Pure functions, stores, and any non-server logic test directly with bun test — no Mochi-specific setup:

// 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');
});
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:

// scripts/run-tests.ts
#!/usr/bin/env bun
import { runTests } from 'mochi-framework';

await runTests();

Point your test script at it:

// package.json
{
  "scripts": {
    "test": "bun scripts/run-tests.ts"
  }
}
bun run test

Each file gets a fresh process, so every test can call Mochi.serve({ port: 0 }) without colliding:

// 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:
await runTests({ sequential: ['src/liveReload.test.ts'] });

runTests exits the process with code 1 if any file fails, so it drops straight into CI.