Fixture Architecture Explained
Fixture Architecture Explained
Section titled “Fixture Architecture Explained”Fixture architecture is TEA’s pattern for building reusable, testable, and composable test utilities. The core principle: build pure functions first, wrap in framework fixtures second.
Overview
Section titled “Overview”The Pattern:
- Write utility as pure function (unit-testable)
- Wrap in framework fixture (Playwright, Cypress)
- Compose fixtures with mergeTests (combine capabilities)
- Package for reuse across projects
Why this order?
- Pure functions are easier to test
- Fixtures depend on framework (less portable)
- Composition happens at fixture level
- Reusability maximized
Fixture Architecture Flow
Section titled “Fixture Architecture Flow”%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%flowchart TD Start([Testing Need]) --> Pure[Step 1: Pure Function<br/>helpers/api-request.ts] Pure -->|Unit testable<br/>Framework agnostic| Fixture[Step 2: Fixture Wrapper<br/>fixtures/api-request.ts] Fixture -->|Injects framework<br/>dependencies| Compose[Step 3: Composition<br/>fixtures/index.ts] Compose -->|mergeTests| Use[Step 4: Use in Tests<br/>tests/**.spec.ts]
Pure -.->|Can test in isolation| UnitTest[Unit Tests<br/>No framework needed] Fixture -.->|Reusable pattern| Other[Other Projects<br/>Package export] Compose -.->|Combine utilities| Multi[Multiple Fixtures<br/>One test]
style Pure fill:#e3f2fd,stroke:#1565c0,stroke-width:2px style Fixture fill:#fff3e0,stroke:#e65100,stroke-width:2px style Compose fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px style Use fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px style UnitTest fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px style Other fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px style Multi fill:#c8e6c9,stroke:#2e7d32,stroke-width:1pxBenefits at Each Step:
- Pure Function: Testable, portable, reusable
- Fixture: Framework integration, clean API
- Composition: Combine capabilities, flexible
- Usage: Simple imports, type-safe
The Problem
Section titled “The Problem”Framework-First Approach (Common Anti-Pattern)
Section titled “Framework-First Approach (Common Anti-Pattern)”// ❌ Bad: Built as fixture from the startexport const test = base.extend({ apiRequest: async ({ request }, use) => { await use(async (options) => { const response = await request.fetch(options.url, { method: options.method, data: options.data });
if (!response.ok()) { throw new Error(`API request failed: ${response.status()}`); }
return response.json(); }); }});Problems:
- Cannot unit test (requires Playwright context)
- Tied to framework (not reusable in other tools)
- Hard to compose with other fixtures
- Difficult to mock for testing the utility itself
Copy-Paste Utilities
Section titled “Copy-Paste Utilities”test('test 1', async ({ request }) => { const response = await request.post('/api/users', { data: {...} }); const body = await response.json(); if (!response.ok()) throw new Error('Failed'); // ... repeated in every test});
// test-2.spec.tstest('test 2', async ({ request }) => { const response = await request.post('/api/users', { data: {...} }); const body = await response.json(); if (!response.ok()) throw new Error('Failed'); // ... same code repeated});Problems:
- Code duplication (violates DRY)
- Inconsistent error handling
- Hard to update (change 50 tests)
- No shared behavior
The Solution: Three-Step Pattern
Section titled “The Solution: Three-Step Pattern”Step 1: Pure Function
Section titled “Step 1: Pure Function”/** * Make API request with automatic error handling * Pure function - no framework dependencies */export async function apiRequest({ request, // Passed in (dependency injection) method, url, data, headers = {}}: ApiRequestParams): Promise<ApiResponse> { const response = await request.fetch(url, { method, data, headers });
if (!response.ok()) { throw new Error(`API request failed: ${response.status()}`); }
return { status: response.status(), body: await response.json() };}
// ✅ Can unit test this function!describe('apiRequest', () => { it('should throw on non-OK response', async () => { const mockRequest = { fetch: vi.fn().mockResolvedValue({ ok: () => false, status: () => 500 }) };
await expect(apiRequest({ request: mockRequest, method: 'GET', url: '/api/test' })).rejects.toThrow('API request failed: 500'); });});Benefits:
- Unit testable (mock dependencies)
- Framework-agnostic (works with any HTTP client)
- Easy to reason about (pure function)
- Portable (can use in Node scripts, CLI tools)
Step 2: Fixture Wrapper
Section titled “Step 2: Fixture Wrapper”import { test as base } from '@playwright/test';import { apiRequest as apiRequestFn } from '../helpers/api-request';
/** * Playwright fixture wrapping the pure function */export const test = base.extend<{ apiRequest: typeof apiRequestFn }>({ apiRequest: async ({ request }, use) => { // Inject framework dependency (request) await use((params) => apiRequestFn({ request, ...params })); }});
export { expect } from '@playwright/test';Benefits:
- Fixture provides framework context (request)
- Pure function handles logic
- Clean separation of concerns
- Can swap frameworks (Cypress, etc.) by changing wrapper only
Step 3: Composition with mergeTests
Section titled “Step 3: Composition with mergeTests”import { mergeTests } from '@playwright/test';import { test as apiRequestTest } from './api-request';import { test as authSessionTest } from './auth-session';import { test as logTest } from './log';
/** * Compose all fixtures into one test */export const test = mergeTests( apiRequestTest, authSessionTest, logTest);
export { expect } from '@playwright/test';Usage:
import { test, expect } from '../support/fixtures';
test('should update profile', async ({ apiRequest, authToken, log }) => { log.info('Starting profile update test');
// Use API request fixture (matches pure function signature) const { status, body } = await apiRequest({ method: 'PATCH', url: '/api/profile', data: { name: 'New Name' }, headers: { Authorization: `Bearer ${authToken}` } });
expect(status).toBe(200); expect(body.name).toBe('New Name');
log.info('Profile updated successfully');});Note: This example uses the vanilla pure function signature (url, data). Playwright Utils uses different parameter names (path, body). See Integrate Playwright Utils for the utilities API.
Note: authToken requires auth-session fixture setup with provider configuration. See auth-session documentation.
Benefits:
- Use multiple fixtures in one test
- No manual composition needed
- Type-safe (TypeScript knows all fixture types)
- Clean imports
How It Works in TEA
Section titled “How It Works in TEA”TEA Generates This Pattern
Section titled “TEA Generates This Pattern”When you run framework with tea_use_playwright_utils: true:
TEA scaffolds:
tests/├── support/│ ├── helpers/ # Pure functions│ │ ├── api-request.ts│ │ └── auth-session.ts│ └── fixtures/ # Framework wrappers│ ├── api-request.ts│ ├── auth-session.ts│ └── index.ts # Composition└── e2e/ └── example.spec.ts # Uses composed fixturesTEA Reviews Against This Pattern
Section titled “TEA Reviews Against This Pattern”When you run test-review:
TEA checks:
- Are utilities pure functions? ✓
- Are fixtures minimal wrappers? ✓
- Is composition used? ✓
- Can utilities be unit tested? ✓
Package Export Pattern
Section titled “Package Export Pattern”Make Fixtures Reusable Across Projects
Section titled “Make Fixtures Reusable Across Projects”Option 1: Build Your Own (Vanilla)
{ "name": "@company/test-utils", "exports": { "./api-request": "./fixtures/api-request.ts", "./auth-session": "./fixtures/auth-session.ts", "./log": "./fixtures/log.ts" }}Usage:
import { test as apiTest } from '@company/test-utils/api-request';import { test as authTest } from '@company/test-utils/auth-session';import { mergeTests } from '@playwright/test';
export const test = mergeTests(apiTest, authTest);Option 2: Use Playwright Utils (Recommended)
npm install -D @seontechnologies/playwright-utilsUsage:
import { test as base } from '@playwright/test';import { mergeTests } from '@playwright/test';import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';import { createAuthFixtures } from '@seontechnologies/playwright-utils/auth-session';
const authFixtureTest = base.extend(createAuthFixtures());export const test = mergeTests(apiRequestFixture, authFixtureTest);// Production-ready utilities, battle-tested!Note: Auth-session requires provider configuration. See auth-session setup guide.
Why Playwright Utils:
- Already built, tested, and maintained
- Consistent patterns across projects
- 11 utilities available (API, auth, network, logging, files)
- Community support and documentation
- Regular updates and improvements
When to Build Your Own:
- Company-specific patterns
- Custom authentication systems
- Unique requirements not covered by utilities
Comparison: Good vs Bad Patterns
Section titled “Comparison: Good vs Bad Patterns”Anti-Pattern: God Fixture
Section titled “Anti-Pattern: God Fixture”// ❌ Bad: Everything in one fixtureexport const test = base.extend({ testUtils: async ({ page, request, context }, use) => { await use({ // 50 different methods crammed into one fixture apiRequest: async (...) => { }, login: async (...) => { }, createUser: async (...) => { }, deleteUser: async (...) => { }, uploadFile: async (...) => { }, // ... 45 more methods }); }});Problems:
- Cannot test individual utilities
- Cannot compose (all-or-nothing)
- Cannot reuse specific utilities
- Hard to maintain (1000+ line file)
Good Pattern: Single-Concern Fixtures
Section titled “Good Pattern: Single-Concern Fixtures”// ✅ Good: One concern per fixture
export const test = base.extend({ apiRequest });
// auth-session.tsexport const test = base.extend({ authSession });
// log.tsexport const test = base.extend({ log });
// Compose as neededimport { mergeTests } from '@playwright/test';export const test = mergeTests(apiRequestTest, authSessionTest, logTest);Benefits:
- Each fixture is unit-testable
- Compose only what you need
- Reuse individual fixtures
- Easy to maintain (small files)
Technical Implementation
Section titled “Technical Implementation”For detailed fixture architecture patterns, see the knowledge base:
When to Use This Pattern
Section titled “When to Use This Pattern”Always Use For:
Section titled “Always Use For:”Reusable utilities:
- API request helpers
- Authentication handlers
- File operations
- Network mocking
Test infrastructure:
- Shared fixtures across teams
- Packaged utilities (playwright-utils)
- Company-wide test standards
Consider Skipping For:
Section titled “Consider Skipping For:”One-off test setup:
// Simple one-time setup - inline is finetest.beforeEach(async ({ page }) => { await page.goto('/'); await page.click('#accept-cookies');});Test-specific helpers:
// Used in one test file only - keep localfunction createTestUser(name: string) { return { name, email: `${name}@test.com` };}Related Concepts
Section titled “Related Concepts”Core TEA Concepts:
- Test Quality Standards - Quality standards fixtures enforce
- Knowledge Base System - Fixture patterns in knowledge base
Technical Patterns:
- Network-First Patterns - Network fixtures explained
- Risk-Based Testing - Fixture complexity matches risk
Overview:
- TEA Overview - Fixture architecture in workflows
- Testing as Engineering - Why fixtures matter
Practical Guides
Section titled “Practical Guides”Setup Guides:
- How to Set Up Test Framework - TEA scaffolds fixtures
- Integrate Playwright Utils - Production-ready fixtures
Workflow Guides:
- How to Run ATDD - Using fixtures in tests
- How to Run Automate - Fixture composition examples
Reference
Section titled “Reference”- TEA Command Reference -
frameworkcommand - Knowledge Base Index - Fixture architecture fragments
- Glossary - Fixture architecture term
Generated with BMad Method - TEA (Test Architect)