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.
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.
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:
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.
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:
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
readythrow. Awaitsync.readyin each test, or make a helper that does. - Use the same schema as the app. Import it; the tests then check what ships.