Factories and Fixtures
The data problem
Every integration test needs data. The GET tests seed books. The POST tests seed authors. The review tests seed books AND authors AND reviews. Copy-pasting INSERT statements into every test file is tedious and fragile — change the schema and you update dozens of tests.
Factory functions
A factory creates test data with sensible defaults. Override only what you need:
// tests/helpers/factories.ts
import { testDb } from "../setup.js";
let counter = 0;
function nextId(prefix: string): string {
counter++;
return `${prefix}-${counter}`;
}
export function createAuthor(overrides: Partial<{ id: string; name: string; bio: string }> = {}) {
const id = overrides.id ?? nextId("author");
const name = overrides.name ?? `Author ${id}`;
const bio = overrides.bio ?? null;
testDb.prepare("INSERT INTO authors (id, name, bio) VALUES (?, ?, ?)").run(id, name, bio);
return { id, name, bio };
}
export function createBook(
overrides: Partial<{
id: string;
title: string;
authorId: string;
genre: string;
description: string;
}> = {},
) {
// Create an author if none specified
const authorId = overrides.authorId ?? createAuthor().id;
const id = overrides.id ?? nextId("book");
const title = overrides.title ?? `Book ${id}`;
const genre = overrides.genre ?? "fiction";
const description = overrides.description ?? null;
testDb
.prepare("INSERT INTO books (id, title, author_id, genre, description) VALUES (?, ?, ?, ?, ?)")
.run(id, title, authorId, genre, description);
return { id, title, authorId, genre, description };
}
export function createReview(
overrides: Partial<{
id: string;
bookId: string;
userId: string;
rating: number;
body: string;
}> = {},
) {
const bookId = overrides.bookId ?? createBook().id;
const id = overrides.id ?? nextId("review");
const userId = overrides.userId ?? nextId("user");
const rating = overrides.rating ?? 4;
const body = overrides.body ?? null;
testDb
.prepare("INSERT INTO reviews (id, book_id, user_id, rating, body) VALUES (?, ?, ?, ?, ?)")
.run(id, bookId, userId, rating, body);
return { id, bookId, userId, rating, body };
} Notice: createBook automatically creates an author if none is specified. createReview automatically creates a book (which creates an author). Dependencies are handled automatically.
Using factories in tests
describe("GET /v2/books/:id", () => {
test("returns book with ratings", async () => {
const book = createBook({ title: "Kindred" });
createReview({ bookId: book.id, rating: 5 });
createReview({ bookId: book.id, rating: 3 });
const response = await app.fetch(new Request(`http://localhost/v2/books/${book.id}`));
const data = await response.json();
expect(data.title).toBe("Kindred");
expect(data.ratings.average).toBe(4);
expect(data.ratings.count).toBe(2);
});
}); Three lines of setup instead of six INSERT statements. The test reads clearly: create a book, add two reviews, check the result.
Resetting between tests
Reset the counter and database in beforeEach:
import { beforeEach } from "vitest";
import { testDb } from "../setup.js";
beforeEach(() => {
testDb.exec("DELETE FROM reviews; DELETE FROM books; DELETE FROM authors;");
}); Each test starts with an empty database. Factories create only the data that test needs.
Fixtures for complex scenarios
For tests that need a specific complex state, create a fixture function:
export function seedCatalog() {
const butler = createAuthor({ name: "Octavia Butler" });
const borges = createAuthor({ name: "Jorge Luis Borges" });
const kindred = createBook({ title: "Kindred", authorId: butler.id, genre: "science-fiction" });
const ficciones = createBook({ title: "Ficciones", authorId: borges.id, genre: "fiction" });
createReview({ bookId: kindred.id, rating: 5 });
createReview({ bookId: kindred.id, rating: 4 });
createReview({ bookId: ficciones.id, rating: 5 });
return { butler, borges, kindred, ficciones };
} test("returns books ordered by title", async () => {
seedCatalog();
const response = await app.fetch(new Request("http://localhost/v2/books"));
const data = await response.json();
expect(data[0].title).toBe("Ficciones");
expect(data[1].title).toBe("Kindred");
}); Exercises
Exercise 1: Create factory functions for authors, books, and reviews. Use them in three tests.
Exercise 2: Create a seedCatalog fixture. Use it in tests that need a populated catalog.
Exercise 3: Add beforeEach to clear the database. Verify tests are independent — each passes alone and together.
Why should factories automatically create dependencies (e.g., createBook creates an author)?