Skip to content
🤖 Consolidated, AI-optimized BMAD docs: llms-full.txt. Fetch this plain text file for complete context.
🚀 Build your own BMad modules and share them with the community! Get started or submit to the marketplace.

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.

The Pattern:

  1. Write utility as pure function (unit-testable)
  2. Wrap in framework fixture (Playwright, Cypress)
  3. Compose fixtures with mergeTests (combine capabilities)
  4. 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
%%{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:1px

Benefits at Each Step:

  1. Pure Function: Testable, portable, reusable
  2. Fixture: Framework integration, clean API
  3. Composition: Combine capabilities, flexible
  4. Usage: Simple imports, type-safe

Framework-First Approach (Common Anti-Pattern)

Section titled “Framework-First Approach (Common Anti-Pattern)”
// ❌ Bad: Built as fixture from the start
export 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
test-1.spec.ts
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.ts
test('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
helpers/api-request.ts
/**
* 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)
fixtures/api-request.ts
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
fixtures/index.ts
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:

tests/profile.spec.ts
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

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 fixtures

When you run test-review:

TEA checks:

  • Are utilities pure functions? ✓
  • Are fixtures minimal wrappers? ✓
  • Is composition used? ✓
  • Can utilities be unit tested? ✓

Option 1: Build Your Own (Vanilla)

package.json
{
"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)

Terminal window
npm install -D @seontechnologies/playwright-utils

Usage:

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
// ❌ Bad: Everything in one fixture
export 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)
api-request.ts
// ✅ Good: One concern per fixture
export const test = base.extend({ apiRequest });
// auth-session.ts
export const test = base.extend({ authSession });
// log.ts
export const test = base.extend({ log });
// Compose as needed
import { 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)

For detailed fixture architecture patterns, see the knowledge base:

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

One-off test setup:

// Simple one-time setup - inline is fine
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.click('#accept-cookies');
});

Test-specific helpers:

// Used in one test file only - keep local
function createTestUser(name: string) {
return { name, email: `${name}@test.com` };
}

Core TEA Concepts:

Technical Patterns:

Overview:

Setup Guides:

Workflow Guides:


Generated with BMad Method - TEA (Test Engineering Architect)