The TypeScript course covered factories at the type-system level: a function that produces a typed object with sensible defaults, accepting overrides for the fields the test cares about. This lesson takes the same idea into a production Cypress framework — typed factories for users, products, and orders, dynamic uniqueness so parallel tests don't collide, API-seeded setup that's deterministic by construction, and the cleanup strategies that keep a shared staging database from filling with test debris.
Why factories beat fixed fixtures for setup
Fixtures are great for stub data — cy.intercept(url, { fixture: "products.json" }) works because every test wants the same 12 products. But fixtures are bad for seed data — every test that creates a real user via cy.request("POST", "/api/users", fixtureUser) runs into "email already exists" the second time it runs.
A factory solves the collision: produces a typed user with a unique-per-call email, accepts overrides for the fields the test specifically wants to set, leaves everything else at sensible defaults.
// cypress/utils/factories.ts
import type { User, Product } from "../support/types";
let counter = 0;
export function createUser(overrides: Partial<User> = {}): User {
counter++;
return {
id: counter,
name: `Test User ${counter}`,
email: `testuser-${counter}-${Date.now()}@test.com`,
role: "standard",
...overrides,
};
}
export function createProduct(overrides: Partial<Product> = {}): Product {
return {
id: counter++,
name: `Test Product ${counter}`,
price: 9.99,
category: "electronics",
inStock: true,
...overrides,
};
}Three properties make this work:
Partial<User>overrides — the test names only the fields it cares about; the rest get defaults.- Spread order matters —
...overrideslast so the test always wins over the default. Date.now()+ counter — guarantees uniqueness even across parallel CI workers.
The factory is the layer where "test data" becomes "test context."
Dynamic uniqueness
Two tests on two parallel CI workers both call createUser({ role: "admin" }) at the same millisecond. Without uniqueness, both produce testuser-1@test.com — the second cy.request("POST", "/api/users", ...) collides on the unique email constraint.
The combination of counter + Date.now() typically gives enough uniqueness:
email: `testuser-${counter}-${Date.now()}@test.com`,Counter changes within the spec; Date.now() changes across specs. Two workers that do hit the same millisecond differentiate by counter; two specs that hit the same counter differentiate by timestamp.
For paranoid uniqueness or large parallel suites, swap in a UUID:
import { v4 as uuid } from "uuid";
email: `testuser-${uuid().slice(0, 8)}@test.com`,@faker-js/faker is the other go-to — it generates plausible names, addresses, phone numbers, and credit cards that look like real data:
import { faker } from "@faker-js/faker";
export function createUser(overrides: Partial<User> = {}): User {
return {
id: faker.number.int({ min: 100_000, max: 999_999 }),
name: faker.person.fullName(),
email: faker.internet.email().toLowerCase(),
role: "standard",
...overrides,
};
}Faker-generated data is also useful when you want test data to look like the real product. A bug report screenshot showing "Test User 1" looks artificial; one showing "Marcus Hahn" looks like a real customer.
Using factories in specs
The factory replaces both inline objects and JSON fixtures in seed-style setup:
import { createUser, createProduct } from "../utils/factories";
import { getApiUrl } from "../utils/api";
describe("User profile", () => {
it("displays the user's name on the profile page", () => {
const user = createUser({ name: "Alice Smith", role: "admin" });
cy.request("POST", getApiUrl("/test/users"), user);
cy.sessionLogin(user.email, "TestPass1!");
cy.visit("/profile");
cy.get("[data-testid='user-name']").should("contain", "Alice Smith");
});
});Three lines of setup; one line of assertion. The factory is what makes the setup readable — a casual reader sees "create an admin user named Alice Smith" without parsing a 30-line fixture.
API-seeded setup with typed factories
The combination of factories and the cy.request patterns from chapter 4 is what most production Cypress suites converge on for setup:
beforeEach(() => {
const user = createUser({ role: "admin" });
cy.request<{ data: User }>("POST", getApiUrl("/test/users"), user)
.its("body.data")
.as("testUser");
cy.sessionLogin(user.email, "TestPass1!");
});
it("displays the new admin in the user list", function () {
cy.visit("/admin/users");
cy.get("[data-testid='user-row']").should("contain", this.testUser.email);
});The user is created via API (fast), aliased as this.testUser (typed via the generic on cy.request), and a session login attaches the auth cookie. The test body is now purely about the assertion — every other line was setup or framework boilerplate.
Factories for relationships
Real domains have entities that compose: an order has a user and a list of products. Factory composition keeps the spec clean:
export function createOrder(overrides: Partial<Order> = {}): Order {
return {
id: ++counter,
userId: 1,
items: [],
total: 0,
status: "pending",
...overrides,
};
}
export function createOrderWithProducts(
productCount = 2,
): { order: Order; products: Product[] } {
const products = Array.from({ length: productCount }, () => createProduct());
const order = createOrder({
items: products.map((p) => ({ productId: p.id, quantity: 1 })),
total: products.reduce((sum, p) => sum + p.price, 0),
});
return { order, products };
}it("displays an order summary with three items", () => {
const { order, products } = createOrderWithProducts(3);
cy.request("POST", getApiUrl("/test/orders"), order);
cy.visit(`/orders/${order.id}`);
cy.get("[data-testid='order-item']").should("have.length", 3);
products.forEach((p) => {
cy.get("[data-testid='order-summary']").should("contain", p.name);
});
});The factory handles the relationship; the spec describes the assertion. Anything tricky (computing the order total, ensuring product IDs match) lives in the factory once.
Cleanup strategies
Three patterns, in increasing operational cost:
- Database reset between runs. A CI-only
cy.taskthat calls a "reset test database" endpoint before the suite. Best for isolated CI environments where you control the backend. Doesn't scale to shared staging environments. - Per-test cleanup in
afterEach. Each test deletes the data it created. Works on shared environments, but slows the suite by ~10% and breaks if a test fails before reaching cleanup. - No cleanup, rely on uniqueness. Tests create unique-per-run rows; the database fills up over time but never collides. Good for short-lived environments; bad for long-lived staging without periodic pruning.
A pragmatic middle ground:
afterEach(function () {
if (this.testUser?.id) {
cy.request("DELETE", getApiUrl(`/test/users/${this.testUser.id}`), {
failOnStatusCode: false,
});
}
});failOnStatusCode: false so a 404 (data already gone) doesn't fail the test. Pair this with a nightly database-cleanup cron on the staging environment to catch anything that slipped through.
The factory-driven test flow
Step 1 of 5
Build test data
createUser({ role: 'admin' }) and createProduct({ price: 49.99 }) — typed objects with unique-per-call IDs.
A complete e-commerce factory file
The full slice as it would appear in a production project:
// cypress/utils/factories.ts
import { faker } from "@faker-js/faker";
import type { User, Product, Order, CartItem } from "../support/types";
let counter = 0;
export function createUser(overrides: Partial<User> = {}): User {
counter++;
return {
id: counter + Date.now(),
name: faker.person.fullName(),
email: faker.internet.email().toLowerCase(),
role: "standard",
...overrides,
};
}
export function createProduct(overrides: Partial<Product> = {}): Product {
counter++;
return {
id: counter + Date.now(),
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
category: faker.commerce.department().toLowerCase(),
inStock: true,
...overrides,
};
}
export function createCartItem(productId: number, quantity = 1): CartItem {
return { productId, quantity };
}
export function createOrder(overrides: Partial<Order> = {}): Order {
counter++;
return {
id: counter + Date.now(),
userId: 1,
items: [],
total: 0,
status: "pending",
...overrides,
};
}
export function createOrderWithProducts(
count = 2,
): { order: Order; products: Product[] } {
const products = Array.from({ length: count }, () => createProduct());
const order = createOrder({
items: products.map((p) => createCartItem(p.id)),
total: products.reduce((s, p) => s + p.price, 0),
});
return { order, products };
}Five factories, one composition helper, every value typed. Any spec in any chapter of this course can now seed an admin user with three products and a draft order in three lines.
⚠️ Common mistakes
- Building a factory that takes thirty arguments. Factories should accept one argument: a
Partial<T>of overrides. Tests pass only what they care about; defaults handle the rest. A function with thirty positional parameters is a sign the abstraction is wrong. - Generating non-unique data. A factory that returns the same email every call works once and fails the second time.
Date.now(),faker.internet.email(), or UUID — pick one and use it consistently. - Putting Cypress chains inside factory functions. Factories should be pure — given the same overrides, they return the same shape. Calling
cy.requestinside a factory mixes synchronous data construction with async test execution and produces confusing race conditions. Build the data with the factory; usecy.requestoutside it to send.
🎯 Practice task
Build a complete e-commerce factory layer. 30-40 minutes.
- Install faker:
npm install --save-dev @faker-js/faker. Addimport { faker } from "@faker-js/faker"tocypress/utils/factories.ts. - Define typed factories for
User,Product,CartItem, andOrdermatching the interfaces incypress/support/types.ts(chapter 9, lesson 2). - Add a composition factory
createOrderWithProducts(count: number)that returns both the order and the underlying product list. - Refactor one of your existing checkout specs to use the factories for setup. The spec should look something like:
const { order, products } = createOrderWithProducts(3); cy.request("POST", ...);— three lines of setup, then assertions. - Uniqueness drill — run the same spec ten times in a row. Confirm no "email already exists" or "product ID conflict" errors appear.
- Add cleanup — wire an
afterEachthat deletes created rows via API. UsefailOnStatusCode: falseto ignore 404s. Confirm the database doesn't accumulate test rows after ten runs. - Stretch: make a typed
cy.seedFromFactory<T>custom command that combines factory + API seed in one call:cy.seedFromFactory(createUser, { role: "admin" })should hit the right endpoint and yield the typed result. The command keeps the factory + API pattern down to a single line in every spec.
The last lesson of chapter 9 (and of the framework portion of the course) covers maintenance — the habits that keep a 200-spec, six-month-old test suite trustworthy instead of letting it drift into the half-ignored test graveyard every long-running project produces.