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#
- Create factory functions: Define functions that generate test objects
- Use builder pattern: For complex objects with many configurations
- Set up database fixtures: Create helpers for seeding test data
- Define scenario fixtures: Combine multiple fixtures for common scenarios
- Clean up between tests: Reset state in beforeEach hooks
Best Practices#
- Use realistic data - Use faker for realistic test data
- Allow overrides - Accept partial objects to customize fixtures
- Keep fixtures focused - Each fixture should have a clear purpose
- Clean between tests - Prevent test pollution
- Compose fixtures - Build complex scenarios from simple fixtures
- Document scenarios - Name fixtures clearly to describe their purpose
- Avoid over-sharing - Don't make fixtures too generic
Related Patterns#
- Vitest - Test runner setup
- Integration Testing - Database testing
- E2E Testing - Playwright fixtures
- Mocking - Mocking dependencies