Testing
Test worker code as ordinary functions, replace the worker in unit tests, and cover the real thing in a browser.
Keep the logic out of the worker file
The methods of a worker are ordinary functions. Put them in their own module, and let the worker file only wire them.
pricing.ts
export interface Line {
price: number;
quantity: number;
}
/** Ordinary functions: nothing here knows about workers. */
export const total = (lines: Line[]) =>
lines.reduce((sum, line) => sum + line.price * line.quantity, 0);
export const average = (lines: Line[]) => (lines.length ? total(lines) / lines.length : 0);pricing.worker.ts
import { defineWorker } from 'easy-web-worker/defineWorker';
import { average, total } from './pricing';
// the worker file only wires the functions; the logic is tested without it
const worker = defineWorker(() => ({ total, average }));
export type PricingWorker = typeof worker;The logic is then tested like any other code, with no worker involved:
pricing.test.ts
import { average, total } from './pricing';
describe('pricing', () => {
const lines = [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 4 },
];
it('adds every line', () => {
expect(total(lines)).toBe(40);
});
it('averages the lines', () => {
expect(average(lines)).toBe(20);
expect(average([])).toBe(0);
});
});Replace the worker in unit tests
Test runners such as Vitest and Jest run in Node or jsdom, where Worker does not exist. Code that uses a worker
should receive it, so a test can hand it a fake one. The type of the worker keeps the fake honest.
format.test.ts
import type { WorkerProxy } from 'easy-web-worker/createWorker';
import type { PricingWorker } from './pricing.worker';
/** The code under test receives the worker, so a test can hand it a fake one. */
const formatTotal = async (pricing: Pick<WorkerProxy<PricingWorker>, 'total'>, prices: number[]) => {
const value = await pricing.total(prices.map((price) => ({ price, quantity: 1 })));
return `$${value.toFixed(2)}`;
};
describe('formatTotal', () => {
it('formats the total computed by the worker', async () => {
const total = vi.fn().mockResolvedValue(12.5);
expect(await formatTotal({ total }, [10, 2.5])).toBe('$12.50');
expect(total).toHaveBeenCalledWith([
{ price: 10, quantity: 1 },
{ price: 2.5, quantity: 1 },
]);
});
});Cover the real thing in a browser
What the unit tests cannot see is the thread boundary itself: structured clone, transferable objects, real cancellation timing, the worker file your bundler produces. Cover those with a browser test.
ts
import { test, expect } from '@playwright/test';
test('computes in a worker', async ({ page }) => {
await page.goto('/');
await page.getByRole('button', { name: 'Run in a Worker' }).click();
await expect(page.getByTestId('worker-result')).toHaveText('39,088,169');
});