Fixtures Pattern

Create and manage test data with factory functions, builder patterns, database seeding, and Playwright fixtures.

Overview#

Test fixtures provide consistent, reusable test data. Good fixtures make tests more readable, maintainable, and reliable by ensuring predictable test scenarios.

When to use:

  • Creating consistent test data across tests
  • Setting up complex object structures
  • Seeding test databases
  • Sharing setup between tests

Key features:

  • Factory functions for object creation
  • Builder pattern for complex objects
  • Database fixtures for integration tests
  • Playwright fixtures for E2E tests

Code Example#

Factory Functions#

1// tests/factories/user.ts 2import { faker } from '@faker-js/faker' 3 4interface UserInput { 5 id?: string 6 email?: string 7 name?: string 8 role?: 'USER' | 'ADMIN' 9 createdAt?: Date 10} 11 12export function createUser(overrides: UserInput = {}) { 13 return { 14 id: faker.string.uuid(), 15 email: faker.internet.email(), 16 name: faker.person.fullName(), 17 role: 'USER' as const, 18 createdAt: faker.date.past(), 19 ...overrides 20 } 21} 22 23// Usage 24const user = createUser({ role: 'ADMIN' }) 25const users = Array.from({ length: 5 }, () => createUser()) 26 27// Factory with relations 28export function createUserWithPosts(overrides: UserInput = {}) { 29 const user = createUser(overrides) 30 return { 31 ...user, 32 posts: Array.from({ length: 3 }, () => createPost({ authorId: user.id })) 33 } 34}

Builder Pattern#

1// tests/factories/post.ts 2import { faker } from '@faker-js/faker' 3 4class PostBuilder { 5 private data = { 6 id: faker.string.uuid(), 7 title: faker.lorem.sentence(), 8 content: faker.lorem.paragraphs(3), 9 published: false, 10 authorId: faker.string.uuid(), 11 createdAt: new Date(), 12 tags: [] as string[] 13 } 14 15 withTitle(title: string) { 16 this.data.title = title 17 return this 18 } 19 20 withContent(content: string) { 21 this.data.content = content 22 return this 23 } 24 25 published() { 26 this.data.published = true 27 return this 28 } 29 30 draft() { 31 this.data.published = false 32 return this 33 } 34 35 byAuthor(authorId: string) { 36 this.data.authorId = authorId 37 return this 38 } 39 40 withTags(...tags: string[]) { 41 this.data.tags = tags 42 return this 43 } 44 45 createdAt(date: Date) { 46 this.data.createdAt = date 47 return this 48 } 49 50 build() { 51 return { ...this.data } 52 } 53} 54 55export const postBuilder = () => new PostBuilder() 56 57// Usage 58const post = postBuilder() 59 .withTitle('My Post') 60 .published() 61 .byAuthor('user-123') 62 .withTags('tech', 'tutorial') 63 .build()

Database Fixtures#

1// tests/fixtures/database.ts 2import { prisma } from '@/lib/db' 3import { createUser } from '../factories/user' 4import { createPost } from '../factories/post' 5 6export async function seedUser(overrides = {}) { 7 const data = createUser(overrides) 8 return prisma.user.create({ data }) 9} 10 11export async function seedUsers(count: number) { 12 const users = Array.from({ length: count }, () => createUser()) 13 return prisma.user.createMany({ data: users }) 14} 15 16export async function seedUserWithPosts(userOverrides = {}, postCount = 3) { 17 const user = await seedUser(userOverrides) 18 const posts = await Promise.all( 19 Array.from({ length: postCount }, () => 20 prisma.post.create({ 21 data: createPost({ authorId: user.id }) 22 }) 23 ) 24 ) 25 return { user, posts } 26} 27 28export async function cleanDatabase() { 29 const tables = ['Comment', 'Post', 'User'] 30 31 for (const table of tables) { 32 await prisma.$executeRawUnsafe(`TRUNCATE TABLE "${table}" CASCADE`) 33 } 34} 35 36// Test setup 37beforeEach(async () => { 38 await cleanDatabase() 39}) 40 41afterAll(async () => { 42 await prisma.$disconnect() 43})

Fixture Files#

1// tests/fixtures/products.json 2[ 3 { 4 "id": "prod_1", 5 "name": "Basic Plan", 6 "price": 999, 7 "currency": "usd" 8 }, 9 { 10 "id": "prod_2", 11 "name": "Pro Plan", 12 "price": 2999, 13 "currency": "usd" 14 } 15] 16 17// tests/fixtures/index.ts 18import products from './products.json' 19 20export const fixtures = { 21 products, 22 users: [ 23 { id: 'user_1', email: 'admin@test.com', role: 'ADMIN' }, 24 { id: 'user_2', email: 'user@test.com', role: 'USER' } 25 ], 26 categories: [ 27 { id: 'cat_1', name: 'Electronics' }, 28 { id: 'cat_2', name: 'Clothing' } 29 ] 30} 31 32// Usage 33import { fixtures } from '../fixtures' 34 35it('displays products', () => { 36 render(<ProductList products={fixtures.products} />) 37 expect(screen.getByText('Basic Plan')).toBeInTheDocument() 38})

Scenario Fixtures#

1// tests/fixtures/scenarios.ts 2import { prisma } from '@/lib/db' 3import { seedUser } from './database' 4 5export async function setupTeamWithMembers() { 6 const owner = await seedUser({ role: 'ADMIN' }) 7 const team = await prisma.team.create({ 8 data: { 9 name: 'Test Team', 10 ownerId: owner.id 11 } 12 }) 13 14 const members = await Promise.all([ 15 seedUser(), 16 seedUser(), 17 seedUser() 18 ]) 19 20 await prisma.teamMember.createMany({ 21 data: members.map(m => ({ 22 userId: m.id, 23 teamId: team.id, 24 role: 'MEMBER' 25 })) 26 }) 27 28 return { owner, team, members } 29} 30 31export async function setupSubscriptionScenario() { 32 const user = await seedUser() 33 const subscription = await prisma.subscription.create({ 34 data: { 35 userId: user.id, 36 planId: 'pro', 37 status: 'active', 38 currentPeriodStart: new Date(), 39 currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) 40 } 41 }) 42 43 return { user, subscription } 44} 45 46export async function setupExpiredSubscription() { 47 const user = await seedUser() 48 const subscription = await prisma.subscription.create({ 49 data: { 50 userId: user.id, 51 planId: 'pro', 52 status: 'past_due', 53 currentPeriodEnd: new Date(Date.now() - 1000) // Expired 54 } 55 }) 56 57 return { user, subscription } 58} 59 60// Usage in test 61it('lists team members', async () => { 62 const { team, members } = await setupTeamWithMembers() 63 64 const result = await getTeamMembers(team.id) 65 expect(result).toHaveLength(members.length) 66})

Playwright Fixtures#

1// tests/e2e/fixtures.ts 2import { test as base, expect } from '@playwright/test' 3import { LoginPage } from './pages/login.page' 4import { DashboardPage } from './pages/dashboard.page' 5 6type Fixtures = { 7 loginPage: LoginPage 8 dashboardPage: DashboardPage 9 authenticatedPage: void 10 testUser: { email: string; password: string } 11} 12 13export const test = base.extend<Fixtures>({ 14 // Page object fixtures 15 loginPage: async ({ page }, use) => { 16 await use(new LoginPage(page)) 17 }, 18 19 dashboardPage: async ({ page }, use) => { 20 await use(new DashboardPage(page)) 21 }, 22 23 // Test data fixture 24 testUser: async ({}, use) => { 25 await use({ 26 email: 'test@example.com', 27 password: 'password123' 28 }) 29 }, 30 31 // Setup fixture that logs in 32 authenticatedPage: async ({ page, testUser }, use) => { 33 await page.goto('/login') 34 await page.fill('[name="email"]', testUser.email) 35 await page.fill('[name="password"]', testUser.password) 36 await page.click('button[type="submit"]') 37 await page.waitForURL('/dashboard') 38 await use() 39 } 40}) 41 42export { expect } 43 44// Usage 45test('authenticated user can access settings', async ({ 46 authenticatedPage, 47 page 48}) => { 49 await page.goto('/settings') 50 await expect(page).toHaveURL('/settings') 51}) 52 53test('user can login', async ({ loginPage, testUser, page }) => { 54 await loginPage.goto() 55 await loginPage.login(testUser.email, testUser.password) 56 await expect(page).toHaveURL('/dashboard') 57})

Fixture Composition#

1// tests/fixtures/compose.ts 2import { createUser } from '../factories/user' 3import { createPost } from '../factories/post' 4import { createComment } from '../factories/comment' 5 6// Compose fixtures for complex scenarios 7export function createBlogPost() { 8 const author = createUser() 9 const post = createPost({ authorId: author.id, published: true }) 10 11 const commenters = [createUser(), createUser()] 12 const comments = commenters.map(user => 13 createComment({ postId: post.id, userId: user.id }) 14 ) 15 16 return { 17 author, 18 post, 19 comments, 20 commenters 21 } 22} 23 24export function createActiveDiscussion() { 25 const participants = Array.from({ length: 5 }, createUser) 26 const author = participants[0] 27 const post = createPost({ authorId: author.id, published: true }) 28 29 const comments = participants.slice(1).flatMap(user => [ 30 createComment({ postId: post.id, userId: user.id }), 31 createComment({ postId: post.id, userId: user.id }) 32 ]) 33 34 return { 35 author, 36 post, 37 comments, 38 participants 39 } 40} 41 42// Usage 43it('displays comment count', () => { 44 const { post, comments } = createBlogPost() 45 46 render(<PostCard post={{ ...post, _count: { comments: comments.length } }} />) 47 48 expect(screen.getByText('2 comments')).toBeInTheDocument() 49})

Usage Instructions#

  1. Create factory functions: Define functions that generate test objects
  2. Use builder pattern: For complex objects with many configurations
  3. Set up database fixtures: Create helpers for seeding test data
  4. Define scenario fixtures: Combine multiple fixtures for common scenarios
  5. Clean up between tests: Reset state in beforeEach hooks

Best Practices#

  1. Use realistic data - Use faker for realistic test data
  2. Allow overrides - Accept partial objects to customize fixtures
  3. Keep fixtures focused - Each fixture should have a clear purpose
  4. Clean between tests - Prevent test pollution
  5. Compose fixtures - Build complex scenarios from simple fixtures
  6. Document scenarios - Name fixtures clearly to describe their purpose
  7. Avoid over-sharing - Don't make fixtures too generic