Appearance
Unit Tests
This guide explains how to test synced-store applications using the provided test harnesses.
Basic Setup with createTestHarness
The createTestHarness function creates a complete testing environment for a single store type with server, client, and network components.
typescript
import { test, expect } from "bun:test";
import { createTestHarness } from "@synced-store/client/test-utils";
import type { MutationContext, JSONValue } from "@synced-store/shared";
const mutators = {
setValue: async (
tx: MutationContext,
arg: { key: string; value: JSONValue },
) => {
await tx.table("main").set(arg.key, arg.value);
},
increment: async (
tx: MutationContext,
arg: { key: string; amount: number },
) => {
const current = (await tx.table("main").get(arg.key)) as number | null;
await tx.table("main").set(arg.key, (current ?? 0) + arg.amount);
},
};
test("basic mutation and query", async () => {
const harness = createTestHarness({ mutators });
const { client } = harness.createClient();
await client.mutate.setValue({ key: "foo", value: "bar" });
const value = await client.query((tx) => tx.table("main").get("foo"));
expect(value).toBe("bar");
});Testing Multiple Clients
Use harness.createClient() to test synchronization between multiple clients:
typescript
test("multiple clients sync data", async () => {
const harness = createTestHarness({ mutators });
const { client: client1 } = harness.createClient();
const { client: client2 } = harness.createClient();
const { confirmed } = await client1.mutate.setValue({
key: "shared",
value: "data",
});
await confirmed;
const value = await client2.query((tx) => tx.table("main").get("shared"));
expect(value).toBe("data");
});Using Network Manager
The networkManager allows you to control all clients' network operations simultaneously:
typescript
test("flush all clients together", async () => {
const harness = createTestHarness({
mutators,
autoFlush: false,
});
const { client: client1, transport: t1 } = harness.createClient();
const { client: client2, transport: t2 } = harness.createClient();
await Promise.all([
t1.flushUntil(client1.waitForServerData()),
t2.flushUntil(client2.waitForServerData()),
]);
await client1.mutate.setValue({ key: "key1", value: "value1" });
await client2.mutate.setValue({ key: "key2", value: "value2" });
await harness.networkManager.stepAll();
});Testing Actions
typescript
const actions = {
bulkSet: async (
ctx,
input: { items: Array<{ key: string; value: JSONValue }> },
) => {
for (const item of input.items) {
await ctx.mutate("setValue", item);
}
return { count: input.items.length };
},
};
test("execute actions", async () => {
const harness = createTestHarness({ mutators, actions });
const { client } = harness.createClient();
const result = await client.action.bulkSet({
items: [
{ key: "a", value: 1 },
{ key: "b", value: 2 },
],
});
expect(result.count).toBe(2);
});Harness Options
createTestHarness Options
| Option | Default | Description |
|---|---|---|
mutators | required | Mutator functions for the server |
actions | {} | Action functions for the server |
injectedServices | {} | Services to inject into action context |
instanceId | "test-space" | Space ID |
autoFlush | true | Auto-flush network requests |
autoPush | true | Auto-push mutations |
retryDelayMultiplierInMs | 0 | Retry delay multiplier |
optimisticUserId | — | UserId for optimistic operations |
createKvStorage | — | Factory for KV storage |
createDeviceChannel | — | Factory for device channels |
harness.createClient() Options
| Option | Default | Description |
|---|---|---|
clientId | auto (client-0, client-1, ...) | Client ID |
userId | auto (user-0, user-1, ...) | User ID |
deviceId | random UUID | Device ID |
onKicked | — | Callback when client is kicked |
onAuthFailed | — | Callback when auth fails |
onDisposed | — | Callback when client is disposed |