JavaScript Test Data Generator: Typed Factories With faker-js and Fishery
`@faker-js/faker` generates realistic values for individual fields, but faker's docs explicitly say you still need to write a factory function for objects. This guide shows how to pair faker with Fishery's typed `Factory.define<T>` so TypeScript enforces your fixture contract at compile time, and how to keep both seed and sequence in step across tests.
Libraries confirmed
TypeScript type safety as the fixture contract
Runnable code sample
Verified against library docs retrieved 2026-08-01. Copy into your test tree and adjust domain vocabulary as needed.
// factories/order.ts
import { faker } from '@faker-js/faker';
import { Factory } from 'fishery';
type Channel = 'web' | 'ios' | 'partner-api';
export interface Order {
id: string;
channel: Channel;
customerEmail: string;
totalCents: number;
placedAt: Date;
}
export const orderFactory = Factory.define<Order>(({ sequence, params }) => {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
return {
id: `order-${sequence}`,
channel: faker.helpers.arrayElement(
['web', 'ios', 'partner-api'] satisfies Channel[],
),
customerEmail:
params.customerEmail ?? faker.internet.email({ firstName, lastName }),
totalCents: faker.number.int({ min: 500, max: 250000 }),
placedAt: faker.date.soon({ refDate: '2026-01-01T00:00:00.000Z' }),
};
});
// order.test.ts
import { beforeEach, expect, test } from 'vitest';
import { faker } from '@faker-js/faker';
import { orderFactory } from './factories/order';
beforeEach(() => {
faker.seed(1337);
orderFactory.rewindSequence();
});
test('a caller supplied email survives the factory default', () => {
const order = orderFactory.build({
channel: 'partner-api',
customerEmail: '[email protected]',
});
expect(order.channel).toBe('partner-api');
expect(order.customerEmail).toBe('[email protected]');
});When to use Generate-Data instead
Frequently asked questions
Why does `faker.seed()` still give me different dates?
Six methods in `@faker-js/faker` are relative to today: `faker.date.past`, `faker.date.future`, `faker.date.recent`, `faker.date.soon`, `faker.git.commitEntry`, and `faker.string.uuid({ version: 7 })`. The "Reproducible results" section of https://fakerjs.dev/guide/usage.html states that "setting a random seed is not sufficient to have reproducible results" for these methods and recommends passing an explicit `refDate` argument or calling `faker.setDefaultRefDate(...)` before your suite runs. Both were confirmed on 2026-08-01.
Should I use faker directly or wrap it in a factory?
Wrap it. The "Create complex objects" section of https://fakerjs.dev/guide/usage.html states "Faker mostly generates values for primitives" and that "if you want to create an object, you most likely need to write a factory function." The Fishery README shows why a typed wrapper adds value beyond convenience: building with an unknown key is a compile error ("Argument of type '{ foo: string; }' is not assignable to parameter of type 'Partial<User>'"), so a renamed field becomes a caught mistake rather than a silently-wrong fixture. Both observed 2026-08-01.
Can I run faker in the browser for a demo?
You can, but the "Browser" section of https://fakerjs.dev/guide/usage.html warns that the full package is "> 5 MiB" minified and "should not be deployed to a web app." The section supplies an `esm.sh` import for a quick in-browser demo, but recommends against shipping it as part of a production bundle. Observed 2026-08-01.