Skip to content

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

OptionDefaultDescription
mutatorsrequiredMutator functions for the server
actions{}Action functions for the server
injectedServices{}Services to inject into action context
instanceId"test-space"Space ID
autoFlushtrueAuto-flush network requests
autoPushtrueAuto-push mutations
retryDelayMultiplierInMs0Retry delay multiplier
optimisticUserIdUserId for optimistic operations
createKvStorageFactory for KV storage
createDeviceChannelFactory for device channels

harness.createClient() Options

OptionDefaultDescription
clientIdauto (client-0, client-1, ...)Client ID
userIdauto (user-0, user-1, ...)User ID
deviceIdrandom UUIDDevice ID
onKickedCallback when client is kicked
onAuthFailedCallback when auth fails
onDisposedCallback when client is disposed