Skip to content
easy-web-worker

Testing

Test worker code as ordinary functions, replace the worker in unit tests, and cover the real thing in a browser.

GuideTypeScriptTry an example

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');
});
Documentation
↑↓ Navigate ↵ Open Esc Closeeasy-web-worker docs
Get started

Real Web Workers. Typed methods.
Cancelable promises.