## 📚 Complete Documentation Restructure **BMM Documentation Hub Created:** - New centralized documentation system at `src/modules/bmm/docs/` - 18 comprehensive guides organized by topic (7000+ lines total) - Clear learning paths for greenfield, brownfield, and quick spec flows - Professional technical writing standards throughout **New Documentation:** - `README.md` - Complete documentation hub with navigation - `quick-start.md` - 15-minute getting started guide - `agents-guide.md` - Comprehensive 12-agent reference (45 min read) - `party-mode.md` - Multi-agent collaboration guide (20 min read) - `scale-adaptive-system.md` - Deep dive on Levels 0-4 (42 min read) - `brownfield-guide.md` - Existing codebase development (53 min read) - `quick-spec-flow.md` - Rapid Level 0-1 development (26 min read) - `workflows-analysis.md` - Phase 1 workflows (12 min read) - `workflows-planning.md` - Phase 2 workflows (19 min read) - `workflows-solutioning.md` - Phase 3 workflows (13 min read) - `workflows-implementation.md` - Phase 4 workflows (33 min read) - `workflows-testing.md` - Testing & QA workflows (29 min read) - `workflow-architecture-reference.md` - Architecture workflow deep-dive - `workflow-document-project-reference.md` - Document-project workflow reference - `enterprise-agentic-development.md` - Team collaboration patterns - `faq.md` - Comprehensive Q&A covering all topics - `glossary.md` - Complete terminology reference - `troubleshooting.md` - Common issues and solutions **Documentation Improvements:** - Removed all version/date footers (git handles versioning) - Agent customization docs now include full rebuild process - Cross-referenced links between all guides - Reading time estimates for all major docs - Consistent professional formatting and structure **Consolidated & Streamlined:** - Module README (`src/modules/bmm/README.md`) streamlined to lean signpost - Root README polished with better hierarchy and clear CTAs - Moved docs from root `docs/` to module-specific locations - Better separation of user docs vs. developer reference ## 🤖 New Agent: Paige (Documentation Guide) **Role:** Technical documentation specialist and information architect **Expertise:** - Professional technical writing standards - Documentation structure and organization - Information architecture and navigation - User-focused content design - Style guide enforcement **Status:** Work in progress - Paige will evolve as documentation needs grow **Integration:** - Listed in agents-guide.md, glossary.md, FAQ - Available for all phases (documentation is continuous) - Can be customized like all BMM agents ## 🔧 Additional Changes - Updated agent manifest with Paige - Updated workflow manifest with new documentation workflows - Fixed workflow-to-agent mappings across all guides - Improved root README with clearer Quick Start section - Better module structure explanations - Enhanced community links with Discord channel names **Total Impact:** - 18 new/restructured documentation files - 7000+ lines of professional technical documentation - Complete navigation system with cross-references - Clear learning paths for all user types - Foundation for knowledge base (coming in beta) Co-Authored-By: Claude <noreply@anthropic.com>
15 KiB
Data Factories and API-First Setup
Principle
Prefer factory functions that accept overrides and return complete objects (createUser(overrides)). Seed test state through APIs, tasks, or direct DB helpers before visiting the UI—never via slow UI interactions. UI is for validation only, not setup.
Rationale
Static fixtures (JSON files, hardcoded objects) create brittle tests that:
- Fail when schemas evolve (missing new required fields)
- Cause collisions in parallel execution (same user IDs)
- Hide test intent (what matters for this test?)
Dynamic factories with overrides provide:
- Parallel safety: UUIDs and timestamps prevent collisions
- Schema evolution: Defaults adapt to schema changes automatically
- Explicit intent: Overrides show what matters for each test
- Speed: API setup is 10-50x faster than UI
Pattern Examples
Example 1: Factory Function with Overrides
Context: When creating test data, build factory functions with sensible defaults and explicit overrides. Use faker for dynamic values that prevent collisions.
Implementation:
// test-utils/factories/user-factory.ts
import { faker } from '@faker-js/faker';
type User = {
id: string;
email: string;
name: string;
role: 'user' | 'admin' | 'moderator';
createdAt: Date;
isActive: boolean;
};
export const createUser = (overrides: Partial<User> = {}): User => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
createdAt: new Date(),
isActive: true,
...overrides,
});
// test-utils/factories/product-factory.ts
type Product = {
id: string;
name: string;
price: number;
stock: number;
category: string;
};
export const createProduct = (overrides: Partial<Product> = {}): Product => ({
id: faker.string.uuid(),
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
stock: faker.number.int({ min: 0, max: 100 }),
category: faker.commerce.department(),
...overrides,
});
// Usage in tests:
test('admin can delete users', async ({ page, apiRequest }) => {
// Default user
const user = createUser();
// Admin user (explicit override shows intent)
const admin = createUser({ role: 'admin' });
// Seed via API (fast!)
await apiRequest({ method: 'POST', url: '/api/users', data: user });
await apiRequest({ method: 'POST', url: '/api/users', data: admin });
// Now test UI behavior
await page.goto('/admin/users');
await page.click(`[data-testid="delete-user-${user.id}"]`);
await expect(page.getByText(`User ${user.name} deleted`)).toBeVisible();
});
Key Points:
Partial<User>allows overriding any field without breaking type safety- Faker generates unique values—no collisions in parallel tests
- Override shows test intent:
createUser({ role: 'admin' })is explicit - Factory lives in
test-utils/factories/for easy reuse
Example 2: Nested Factory Pattern
Context: When testing relationships (orders with users and products), nest factories to create complete object graphs. Control relationship data explicitly.
Implementation:
// test-utils/factories/order-factory.ts
import { createUser } from './user-factory';
import { createProduct } from './product-factory';
type OrderItem = {
product: Product;
quantity: number;
price: number;
};
type Order = {
id: string;
user: User;
items: OrderItem[];
total: number;
status: 'pending' | 'paid' | 'shipped' | 'delivered';
createdAt: Date;
};
export const createOrderItem = (overrides: Partial<OrderItem> = {}): OrderItem => {
const product = overrides.product || createProduct();
const quantity = overrides.quantity || faker.number.int({ min: 1, max: 5 });
return {
product,
quantity,
price: product.price * quantity,
...overrides,
};
};
export const createOrder = (overrides: Partial<Order> = {}): Order => {
const items = overrides.items || [createOrderItem(), createOrderItem()];
const total = items.reduce((sum, item) => sum + item.price, 0);
return {
id: faker.string.uuid(),
user: overrides.user || createUser(),
items,
total,
status: 'pending',
createdAt: new Date(),
...overrides,
};
};
// Usage in tests:
test('user can view order details', async ({ page, apiRequest }) => {
const user = createUser({ email: 'test@example.com' });
const product1 = createProduct({ name: 'Widget A', price: 10.0 });
const product2 = createProduct({ name: 'Widget B', price: 15.0 });
// Explicit relationships
const order = createOrder({
user,
items: [
createOrderItem({ product: product1, quantity: 2 }), // $20
createOrderItem({ product: product2, quantity: 1 }), // $15
],
});
// Seed via API
await apiRequest({ method: 'POST', url: '/api/users', data: user });
await apiRequest({ method: 'POST', url: '/api/products', data: product1 });
await apiRequest({ method: 'POST', url: '/api/products', data: product2 });
await apiRequest({ method: 'POST', url: '/api/orders', data: order });
// Test UI
await page.goto(`/orders/${order.id}`);
await expect(page.getByText('Widget A x 2')).toBeVisible();
await expect(page.getByText('Widget B x 1')).toBeVisible();
await expect(page.getByText('Total: $35.00')).toBeVisible();
});
Key Points:
- Nested factories handle relationships (order → user, order → products)
- Overrides cascade: provide custom user/products or use defaults
- Calculated fields (total) derived automatically from nested data
- Explicit relationships make test data clear and maintainable
Example 3: Factory with API Seeding
Context: When tests need data setup, always use API calls or database tasks—never UI navigation. Wrap factory usage with seeding utilities for clean test setup.
Implementation:
// playwright/support/helpers/seed-helpers.ts
import { APIRequestContext } from '@playwright/test';
import { User, createUser } from '../../test-utils/factories/user-factory';
import { Product, createProduct } from '../../test-utils/factories/product-factory';
export async function seedUser(request: APIRequestContext, overrides: Partial<User> = {}): Promise<User> {
const user = createUser(overrides);
const response = await request.post('/api/users', {
data: user,
});
if (!response.ok()) {
throw new Error(`Failed to seed user: ${response.status()}`);
}
return user;
}
export async function seedProduct(request: APIRequestContext, overrides: Partial<Product> = {}): Promise<Product> {
const product = createProduct(overrides);
const response = await request.post('/api/products', {
data: product,
});
if (!response.ok()) {
throw new Error(`Failed to seed product: ${response.status()}`);
}
return product;
}
// Playwright globalSetup for shared data
// playwright/support/global-setup.ts
import { chromium, FullConfig } from '@playwright/test';
import { seedUser } from './helpers/seed-helpers';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
const context = page.context();
// Seed admin user for all tests
const admin = await seedUser(context.request, {
email: 'admin@example.com',
role: 'admin',
});
// Save auth state for reuse
await context.storageState({ path: 'playwright/.auth/admin.json' });
await browser.close();
}
export default globalSetup;
// Cypress equivalent with cy.task
// cypress/support/tasks.ts
export const seedDatabase = async (entity: string, data: unknown) => {
// Direct database insert or API call
if (entity === 'users') {
await db.users.create(data);
}
return null;
};
// Usage in Cypress tests:
beforeEach(() => {
const user = createUser({ email: 'test@example.com' });
cy.task('db:seed', { entity: 'users', data: user });
});
Key Points:
- API seeding is 10-50x faster than UI-based setup
globalSetupseeds shared data once (e.g., admin user)- Per-test seeding uses
seedUser()helpers for isolation - Cypress
cy.taskallows direct database access for speed
Example 4: Anti-Pattern - Hardcoded Test Data
Problem:
// ❌ BAD: Hardcoded test data
test('user can login', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email"]', 'test@test.com'); // Hardcoded
await page.fill('[data-testid="password"]', 'password123'); // Hardcoded
await page.click('[data-testid="submit"]');
// What if this user already exists? Test fails in parallel runs.
// What if schema adds required fields? Test breaks.
});
// ❌ BAD: Static JSON fixtures
// fixtures/users.json
{
"users": [
{ "id": 1, "email": "user1@test.com", "name": "User 1" },
{ "id": 2, "email": "user2@test.com", "name": "User 2" }
]
}
test('admin can delete user', async ({ page }) => {
const users = require('../fixtures/users.json');
// Brittle: IDs collide in parallel, schema drift breaks tests
});
Why It Fails:
- Parallel collisions: Hardcoded IDs (
id: 1,email: 'test@test.com') cause failures when tests run concurrently - Schema drift: Adding required fields (
phoneNumber,address) breaks all tests using fixtures - Hidden intent: Does this test need
email: 'test@test.com'specifically, or any email? - Slow setup: UI-based data creation is 10-50x slower than API
Better Approach: Use factories
// ✅ GOOD: Factory-based data
test('user can login', async ({ page, apiRequest }) => {
const user = createUser({ email: 'unique@example.com', password: 'secure123' });
// Seed via API (fast, parallel-safe)
await apiRequest({ method: 'POST', url: '/api/users', data: user });
// Test UI
await page.goto('/login');
await page.fill('[data-testid="email"]', user.email);
await page.fill('[data-testid="password"]', user.password);
await page.click('[data-testid="submit"]');
await expect(page).toHaveURL('/dashboard');
});
// ✅ GOOD: Factories adapt to schema changes automatically
// When `phoneNumber` becomes required, update factory once:
export const createUser = (overrides: Partial<User> = {}): User => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
phoneNumber: faker.phone.number(), // NEW field, all tests get it automatically
role: 'user',
...overrides,
});
Key Points:
- Factories generate unique, parallel-safe data
- Schema evolution handled in one place (factory), not every test
- Test intent explicit via overrides
- API seeding is fast and reliable
Example 5: Factory Composition
Context: When building specialized factories, compose simpler factories instead of duplicating logic. Layer overrides for specific test scenarios.
Implementation:
// test-utils/factories/user-factory.ts (base)
export const createUser = (overrides: Partial<User> = {}): User => ({
id: faker.string.uuid(),
email: faker.internet.email(),
name: faker.person.fullName(),
role: 'user',
createdAt: new Date(),
isActive: true,
...overrides,
});
// Compose specialized factories
export const createAdminUser = (overrides: Partial<User> = {}): User => createUser({ role: 'admin', ...overrides });
export const createModeratorUser = (overrides: Partial<User> = {}): User => createUser({ role: 'moderator', ...overrides });
export const createInactiveUser = (overrides: Partial<User> = {}): User => createUser({ isActive: false, ...overrides });
// Account-level factories with feature flags
type Account = {
id: string;
owner: User;
plan: 'free' | 'pro' | 'enterprise';
features: string[];
maxUsers: number;
};
export const createAccount = (overrides: Partial<Account> = {}): Account => ({
id: faker.string.uuid(),
owner: overrides.owner || createUser(),
plan: 'free',
features: [],
maxUsers: 1,
...overrides,
});
export const createProAccount = (overrides: Partial<Account> = {}): Account =>
createAccount({
plan: 'pro',
features: ['advanced-analytics', 'priority-support'],
maxUsers: 10,
...overrides,
});
export const createEnterpriseAccount = (overrides: Partial<Account> = {}): Account =>
createAccount({
plan: 'enterprise',
features: ['advanced-analytics', 'priority-support', 'sso', 'audit-logs'],
maxUsers: 100,
...overrides,
});
// Usage in tests:
test('pro accounts can access analytics', async ({ page, apiRequest }) => {
const admin = createAdminUser({ email: 'admin@company.com' });
const account = createProAccount({ owner: admin });
await apiRequest({ method: 'POST', url: '/api/users', data: admin });
await apiRequest({ method: 'POST', url: '/api/accounts', data: account });
await page.goto('/analytics');
await expect(page.getByText('Advanced Analytics')).toBeVisible();
});
test('free accounts cannot access analytics', async ({ page, apiRequest }) => {
const user = createUser({ email: 'user@company.com' });
const account = createAccount({ owner: user }); // Defaults to free plan
await apiRequest({ method: 'POST', url: '/api/users', data: user });
await apiRequest({ method: 'POST', url: '/api/accounts', data: account });
await page.goto('/analytics');
await expect(page.getByText('Upgrade to Pro')).toBeVisible();
});
Key Points:
- Compose specialized factories from base factories (
createAdminUser→createUser) - Defaults cascade:
createProAccountsets plan + features automatically - Still allow overrides:
createProAccount({ maxUsers: 50 })works - Test intent clear:
createProAccount()vscreateAccount({ plan: 'pro', features: [...] })
Integration Points
- Used in workflows:
*atdd(test generation),*automate(test expansion),*framework(factory setup) - Related fragments:
fixture-architecture.md- Pure functions and fixtures for factory integrationnetwork-first.md- API-first setup patternstest-quality.md- Parallel-safe, deterministic test design
Cleanup Strategy
Ensure factories work with cleanup patterns:
// Track created IDs for cleanup
const createdUsers: string[] = [];
afterEach(async ({ apiRequest }) => {
// Clean up all users created during test
for (const userId of createdUsers) {
await apiRequest({ method: 'DELETE', url: `/api/users/${userId}` });
}
createdUsers.length = 0;
});
test('user registration flow', async ({ page, apiRequest }) => {
const user = createUser();
createdUsers.push(user.id);
await apiRequest({ method: 'POST', url: '/api/users', data: user });
// ... test logic
});
Feature Flag Integration
When working with feature flags, layer them into factories:
export const createUserWithFlags = (
overrides: Partial<User> = {},
flags: Record<string, boolean> = {},
): User & { flags: Record<string, boolean> } => ({
...createUser(overrides),
flags: {
'new-dashboard': false,
'beta-features': false,
...flags,
},
});
// Usage:
const user = createUserWithFlags(
{ email: 'test@example.com' },
{
'new-dashboard': true,
'beta-features': true,
},
);
Source: Murat Testing Philosophy (lines 94-120), API-first testing patterns, faker.js documentation.