Ferry
Client

Testing

The client needs no server and no device to test: give it memory storage and the local() backend, which are the defaults, and it runs anywhere your tests do.

Client
import { createSync } from 'ferrysync';
import { expect, it } from 'vitest';

import { schema } from './schema';

it('completes a task', async () => {
  const sync = createSync({ schema }); // memoryStorage() and local()
  await sync.ready;

  const task = sync.tasks.create({ projectId: 'p1', title: 'Visa' });
  sync.tasks.update(task.id, { done: true });

  expect(sync.tasks.where({ done: true }).get()).toEqual([{ ...task, done: true }]);
  sync.close();
});

Writes apply synchronously, so most tests need no waiting: write, then assert.

Control time

Syncing runs on timers: a write is sent on the next tick, a failed attempt is retried after a backoff. Pass a virtual clock to decide when time moves, so tests never sleep and always run the same way.

Client
import { createSync } from 'ferrysync';
import { createVirtualClock } from 'ferrysync/testing';
import { expect, it } from 'vitest';

import { schema } from './schema';

it('confirms writes on the next tick', async () => {
  const clock = createVirtualClock();
  const sync = createSync({ schema, now: clock.now, scheduler: clock.scheduler });
  await sync.ready;

  sync.projects.create({ name: 'Japan' });
  expect(sync.status).toMatchObject({ pending: 1, synced: false });

  await clock.advance(0); // run what is due now, and everything it starts
  expect(sync.status).toMatchObject({ pending: 0, synced: true });
  sync.close();
});

clock.advance(ms) moves time forward, firing due timers in order and letting the promises they start settle. clock.pending() says how many timers are still waiting.

Predictable ids

Pass createId to make ids you can write in assertions:

Client
import { createSync } from 'ferrysync';

import { schema } from './schema';

let next = 0;
const sync = createSync({ schema, createId: () => `id-${++next}` });
await sync.ready;

sync.projects.create({ name: 'Japan' }).id; // 'id-1'

Ids sort as text, and rows come in id order: pad them (id-0001) when a test creates more than nine rows and reads them in order.

Restarts

Share a memoryStorage() between two clients to test what survives a restart. settle() lets the first client finish writing to storage before it stops.

Client
import { createSync, memoryStorage } from 'ferrysync';
import { settle } from 'ferrysync/testing';
import { expect, it } from 'vitest';

import { schema } from './schema';

it('keeps drafts across restarts', async () => {
  const storage = memoryStorage();
  const before = createSync({ schema, storage });
  await before.ready;
  const draft = before.drafts.create({ text: 'Remember the visa' });
  await settle();
  before.close();

  const after = createSync({ schema, storage });
  await after.ready;
  expect(after.drafts.get(draft.id)).toEqual(draft);
  after.close();
});

memoryStorage also has two helpers for tests: peek() returns a copy of what is stored, and failNextWrite() makes the next write fail, as a full disk would.

Components

Render inside a SyncProvider, wait for the content (the provider shows its fallback until storage has loaded; a client already ready renders at once), and wrap writes in act:

Client
import { act, render, screen } from '@testing-library/react';
import { createSync } from 'ferrysync';
import { SyncProvider, useQuery } from 'ferrysync/react';
import { expect, it } from 'vitest';

import { schema } from './schema';

it('lists projects', async () => {
  const sync = createSync({ schema });
  const Projects = () => <p>Projects: {useQuery(sync.projects.all()).length}</p>;
  render(
    <SyncProvider sync={sync}>
      <Projects />
    </SyncProvider>,
  );
  await screen.findByText('Projects: 0');

  act(() => {
    sync.projects.create({ name: 'Japan' });
  });
  expect(screen.getByText('Projects: 1')).toBeTruthy();
});

With a server

local() confirms everything, so it cannot show what a real server does: rejected writes, other users' changes, spaces. For those, createTestHarness (from ferrysync/testing) runs your server in-process and gives you clients that sync through it. See Testing with a server.

Gotchas

  • Close clients at the end of a test (sync.close()), so their timers stop and nothing of one test runs into the next.
  • Writes before ready throw. Await sync.ready in each test, or make a helper that does.
  • Use the same schema as the app. Import it; the tests then check what ships.

On this page