Use factory functions (e.g., `createUser({role: 'admin'})`) instead of static JSON fixtures. Factories let you create exactly the data each test needs with sensible defaults, while fixtures force you to maintain large JSON files where a change to one test's data breaks another test.
Static JSON fixtures create coupling between tests: every test that imports `fixtures/user.json` depends on the same data shape. When test A needs the user to have an `admin` role and test B needs `viewer`, you either create dozens of nearly-identical fixture files or use a single fixture that doesn't accurately represent either scenario. Factories solve this by generating test data on demand with sensible defaults and per-test overrides.
BeforeMerge scans your pull requests against this rule and 3+ others. Get actionable feedback before code ships.
Static fixtures (JSON files, hardcoded objects) seem simple, but they create problems at scale:
user.json, adminUser.json, userWithoutEmail.json, userWithExpiredSubscription.json.fixtures.user, you must open the fixture file to understand what data the test is working with. The test's intent is hidden.Factories solve all of these by generating test data programmatically with sensible defaults. Each test specifies only the fields that matter for that test.
Create factory functions that generate test data with sensible defaults. Each test overrides only the fields relevant to its scenario. Use a library like fishery or write simple factory functions by hand.
// fixtures/user.json
{
"id": "user-1",
"name": "Test User",
"email": "test@example.com",
"role": "viewer",
"plan": "free",
"createdAt": "2024-01-01T00:00:00Z"
}
// test file
import user from "../fixtures/user.json";
it("allows admin to delete users", () => {
// BAD: fixture has role: "viewer", but this test needs "admin"
// Options: modify the shared fixture (breaks other tests)
// or create adminUser.json (fixture sprawl)
const admin = { ...user, role: "admin" }; // spread-and-override in every test
expect(canDeleteUsers(admin)).toBe(true);
});// test/factories/user.ts
let counter = 0;
export function createUser(overrides: Partial<User> = {}): User {
counter++;
return {
id: `user-${counter}`,
name: `Test User ${counter}`,
email: `user-${counter}@test.com`,
role: "viewer",
plan: "free",
createdAt: new Date().toISOString(),
...overrides,
};
}
// test file — each test specifies exactly what matters
import { createUser } from "../factories/user";
it("allows admin to delete users", () => {
const admin = createUser({ role: "admin" });
expect(canDeleteUsers(admin)).toBe(true);
});
it("prevents viewers from deleting users", () => {
const viewer = createUser({ role: "viewer" });
expect(canDeleteUsers(viewer)).toBe(false);
});
it("requires paid plan for export", () => {
const freeUser = createUser({ plan: "free" });
const paidUser = createUser({ plan: "pro" });
expect(canExport(freeUser)).toBe(false);
expect(canExport(paidUser)).toBe(true);
});Search for fixture imports:
grep -rn "fixtures/" --include="*.test.ts" --include="*.spec.ts" src/
grep -rn "from.*\.json" --include="*.test.ts" --include="*.spec.ts" src/Also look for repeated spread-and-override patterns: { ...baseUser, role: "admin" } appearing in multiple test files.
test/factories/ directory