|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- <!-- Powered by BMAD-CORE™ -->
-
- # Test Levels Framework
-
- Comprehensive guide for determining appropriate test levels (unit, integration, E2E) for different scenarios.
-
- ## Test Level Decision Matrix
-
- ### Unit Tests
-
- **When to use:**
-
- - Testing pure functions and business logic
- - Algorithm correctness
- - Input validation and data transformation
- - Error handling in isolated components
- - Complex calculations or state machines
-
- **Characteristics:**
-
- - Fast execution (immediate feedback)
- - No external dependencies (DB, API, file system)
- - Highly maintainable and stable
- - Easy to debug failures
-
- **Example scenarios:**
-
- ```yaml
- unit_test:
- component: 'PriceCalculator'
- scenario: 'Calculate discount with multiple rules'
- justification: 'Complex business logic with multiple branches'
- mock_requirements: 'None - pure function'
- ```
-
- ### Integration Tests
-
- **When to use:**
-
- - Component interaction verification
- - Database operations and transactions
- - API endpoint contracts
- - Service-to-service communication
- - Middleware and interceptor behavior
-
- **Characteristics:**
-
- - Moderate execution time
- - Tests component boundaries
- - May use test databases or containers
- - Validates system integration points
-
- **Example scenarios:**
-
- ```yaml
- integration_test:
- components: ['UserService', 'AuthRepository']
- scenario: 'Create user with role assignment'
- justification: 'Critical data flow between service and persistence'
- test_environment: 'In-memory database'
- ```
-
- ### End-to-End Tests
-
- **When to use:**
-
- - Critical user journeys
- - Cross-system workflows
- - Visual regression testing
- - Compliance and regulatory requirements
- - Final validation before release
-
- **Characteristics:**
-
- - Slower execution
- - Tests complete workflows
- - Requires full environment setup
- - Most realistic but most brittle
-
- **Example scenarios:**
-
- ```yaml
- e2e_test:
- journey: 'Complete checkout process'
- scenario: 'User purchases with saved payment method'
- justification: 'Revenue-critical path requiring full validation'
- environment: 'Staging with test payment gateway'
- ```
-
- ## Test Level Selection Rules
-
- ### Favor Unit Tests When:
-
- - Logic can be isolated
- - No side effects involved
- - Fast feedback needed
- - High cyclomatic complexity
-
- ### Favor Integration Tests When:
-
- - Testing persistence layer
- - Validating service contracts
- - Testing middleware/interceptors
- - Component boundaries critical
-
- ### Favor E2E Tests When:
-
- - User-facing critical paths
- - Multi-system interactions
- - Regulatory compliance scenarios
- - Visual regression important
-
- ## Anti-patterns to Avoid
-
- - E2E testing for business logic validation
- - Unit testing framework behavior
- - Integration testing third-party libraries
- - Duplicate coverage across levels
-
- ## Duplicate Coverage Guard
-
- **Before adding any test, check:**
-
- 1. Is this already tested at a lower level?
- 2. Can a unit test cover this instead of integration?
- 3. Can an integration test cover this instead of E2E?
-
- **Coverage overlap is only acceptable when:**
-
- - Testing different aspects (unit: logic, integration: interaction, e2e: user experience)
- - Critical paths requiring defense in depth
- - Regression prevention for previously broken functionality
-
- ## Test Naming Conventions
-
- - Unit: `test_{component}_{scenario}`
- - Integration: `test_{flow}_{interaction}`
- - E2E: `test_{journey}_{outcome}`
-
- ## Test ID Format
-
- `{EPIC}.{STORY}-{LEVEL}-{SEQ}`
-
- Examples:
-
- - `1.3-UNIT-001`
- - `1.3-INT-002`
- - `1.3-E2E-001`
-
- ## Real Code Examples
-
- ### Example 1: E2E Test (Full User Journey)
-
- **Scenario**: User logs in, navigates to dashboard, and places an order.
-
- ```typescript
- // tests/e2e/checkout-flow.spec.ts
- import { test, expect } from '@playwright/test';
- import { createUser, createProduct } from '../test-utils/factories';
-
- test.describe('Checkout Flow', () => {
- test('user can complete purchase with saved payment method', async ({ page, apiRequest }) => {
- // Setup: Seed data via API (fast!)
- const user = createUser({ email: 'buyer@example.com', hasSavedCard: true });
- const product = createProduct({ name: 'Widget', price: 29.99, stock: 10 });
-
- await apiRequest.post('/api/users', { data: user });
- await apiRequest.post('/api/products', { data: product });
-
- // Network-first: Intercept BEFORE action
- const loginPromise = page.waitForResponse('**/api/auth/login');
- const cartPromise = page.waitForResponse('**/api/cart');
- const orderPromise = page.waitForResponse('**/api/orders');
-
- // Step 1: Login
- await page.goto('/login');
- await page.fill('[data-testid="email"]', user.email);
- await page.fill('[data-testid="password"]', 'password123');
- await page.click('[data-testid="login-button"]');
- await loginPromise;
-
- // Assert: Dashboard visible
- await expect(page).toHaveURL('/dashboard');
- await expect(page.getByText(`Welcome, ${user.name}`)).toBeVisible();
-
- // Step 2: Add product to cart
- await page.goto(`/products/${product.id}`);
- await page.click('[data-testid="add-to-cart"]');
- await cartPromise;
- await expect(page.getByText('Added to cart')).toBeVisible();
-
- // Step 3: Checkout with saved payment
- await page.goto('/checkout');
- await expect(page.getByText('Visa ending in 1234')).toBeVisible(); // Saved card
- await page.click('[data-testid="use-saved-card"]');
- await page.click('[data-testid="place-order"]');
- await orderPromise;
-
- // Assert: Order confirmation
- await expect(page.getByText('Order Confirmed')).toBeVisible();
- await expect(page.getByText(/Order #\d+/)).toBeVisible();
- await expect(page.getByText('$29.99')).toBeVisible();
- });
- });
- ```
-
- **Key Points (E2E)**:
-
- - Tests complete user journey across multiple pages
- - API setup for data (fast), UI for assertions (user-centric)
- - Network-first interception to prevent flakiness
- - Validates critical revenue path end-to-end
-
- ### Example 2: Integration Test (API/Service Layer)
-
- **Scenario**: UserService creates user and assigns role via AuthRepository.
-
- ```typescript
- // tests/integration/user-service.spec.ts
- import { test, expect } from '@playwright/test';
- import { createUser } from '../test-utils/factories';
-
- test.describe('UserService Integration', () => {
- test('should create user with admin role via API', async ({ request }) => {
- const userData = createUser({ role: 'admin' });
-
- // Direct API call (no UI)
- const response = await request.post('/api/users', {
- data: userData,
- });
-
- expect(response.status()).toBe(201);
-
- const createdUser = await response.json();
- expect(createdUser.id).toBeTruthy();
- expect(createdUser.email).toBe(userData.email);
- expect(createdUser.role).toBe('admin');
-
- // Verify database state
- const getResponse = await request.get(`/api/users/${createdUser.id}`);
- expect(getResponse.status()).toBe(200);
-
- const fetchedUser = await getResponse.json();
- expect(fetchedUser.role).toBe('admin');
- expect(fetchedUser.permissions).toContain('user:delete');
- expect(fetchedUser.permissions).toContain('user:update');
-
- // Cleanup
- await request.delete(`/api/users/${createdUser.id}`);
- });
-
- test('should validate email uniqueness constraint', async ({ request }) => {
- const userData = createUser({ email: 'duplicate@example.com' });
-
- // Create first user
- const response1 = await request.post('/api/users', { data: userData });
- expect(response1.status()).toBe(201);
-
- const user1 = await response1.json();
-
- // Attempt duplicate email
- const response2 = await request.post('/api/users', { data: userData });
- expect(response2.status()).toBe(409); // Conflict
- const error = await response2.json();
- expect(error.message).toContain('Email already exists');
-
- // Cleanup
- await request.delete(`/api/users/${user1.id}`);
- });
- });
- ```
-
- **Key Points (Integration)**:
-
- - Tests service layer + database interaction
- - No UI involved—pure API validation
- - Business logic focus (role assignment, constraints)
- - Faster than E2E, more realistic than unit tests
-
- ### Example 3: Component Test (Isolated UI Component)
-
- **Scenario**: Test button component in isolation with props and user interactions.
-
- ```typescript
- // src/components/Button.cy.tsx (Cypress Component Test)
- import { Button } from './Button';
-
- describe('Button Component', () => {
- it('should render with correct label', () => {
- cy.mount(<Button label="Click Me" />);
- cy.contains('Click Me').should('be.visible');
- });
-
- it('should call onClick handler when clicked', () => {
- const onClickSpy = cy.stub().as('onClick');
- cy.mount(<Button label="Submit" onClick={onClickSpy} />);
-
- cy.get('button').click();
- cy.get('@onClick').should('have.been.calledOnce');
- });
-
- it('should be disabled when disabled prop is true', () => {
- cy.mount(<Button label="Disabled" disabled={true} />);
- cy.get('button').should('be.disabled');
- cy.get('button').should('have.attr', 'aria-disabled', 'true');
- });
-
- it('should show loading spinner when loading', () => {
- cy.mount(<Button label="Loading" loading={true} />);
- cy.get('[data-testid="spinner"]').should('be.visible');
- cy.get('button').should('be.disabled');
- });
-
- it('should apply variant styles correctly', () => {
- cy.mount(<Button label="Primary" variant="primary" />);
- cy.get('button').should('have.class', 'btn-primary');
-
- cy.mount(<Button label="Secondary" variant="secondary" />);
- cy.get('button').should('have.class', 'btn-secondary');
- });
- });
-
- // Playwright Component Test equivalent
- import { test, expect } from '@playwright/experimental-ct-react';
- import { Button } from './Button';
-
- test.describe('Button Component', () => {
- test('should call onClick handler when clicked', async ({ mount }) => {
- let clicked = false;
- const component = await mount(
- <Button label="Submit" onClick={() => { clicked = true; }} />
- );
-
- await component.getByRole('button').click();
- expect(clicked).toBe(true);
- });
-
- test('should be disabled when loading', async ({ mount }) => {
- const component = await mount(<Button label="Loading" loading={true} />);
- await expect(component.getByRole('button')).toBeDisabled();
- await expect(component.getByTestId('spinner')).toBeVisible();
- });
- });
- ```
-
- **Key Points (Component)**:
-
- - Tests UI component in isolation (no full app)
- - Props + user interactions + visual states
- - Faster than E2E, more realistic than unit tests for UI
- - Great for design system components
-
- ### Example 4: Unit Test (Pure Function)
-
- **Scenario**: Test pure business logic function without framework dependencies.
-
- ```typescript
- // src/utils/price-calculator.test.ts (Jest/Vitest)
- import { calculateDiscount, applyTaxes, calculateTotal } from './price-calculator';
-
- describe('PriceCalculator', () => {
- describe('calculateDiscount', () => {
- it('should apply percentage discount correctly', () => {
- const result = calculateDiscount(100, { type: 'percentage', value: 20 });
- expect(result).toBe(80);
- });
-
- it('should apply fixed amount discount correctly', () => {
- const result = calculateDiscount(100, { type: 'fixed', value: 15 });
- expect(result).toBe(85);
- });
-
- it('should not apply discount below zero', () => {
- const result = calculateDiscount(10, { type: 'fixed', value: 20 });
- expect(result).toBe(0);
- });
-
- it('should handle no discount', () => {
- const result = calculateDiscount(100, { type: 'none', value: 0 });
- expect(result).toBe(100);
- });
- });
-
- describe('applyTaxes', () => {
- it('should calculate tax correctly for US', () => {
- const result = applyTaxes(100, { country: 'US', rate: 0.08 });
- expect(result).toBe(108);
- });
-
- it('should calculate tax correctly for EU (VAT)', () => {
- const result = applyTaxes(100, { country: 'DE', rate: 0.19 });
- expect(result).toBe(119);
- });
-
- it('should handle zero tax rate', () => {
- const result = applyTaxes(100, { country: 'US', rate: 0 });
- expect(result).toBe(100);
- });
- });
-
- describe('calculateTotal', () => {
- it('should calculate total with discount and taxes', () => {
- const items = [
- { price: 50, quantity: 2 }, // 100
- { price: 30, quantity: 1 }, // 30
- ];
- const discount = { type: 'percentage', value: 10 }; // -13
- const tax = { country: 'US', rate: 0.08 }; // +9.36
-
- const result = calculateTotal(items, discount, tax);
- expect(result).toBeCloseTo(126.36, 2);
- });
-
- it('should handle empty items array', () => {
- const result = calculateTotal([], { type: 'none', value: 0 }, { country: 'US', rate: 0 });
- expect(result).toBe(0);
- });
-
- it('should calculate correctly without discount or tax', () => {
- const items = [{ price: 25, quantity: 4 }];
- const result = calculateTotal(items, { type: 'none', value: 0 }, { country: 'US', rate: 0 });
- expect(result).toBe(100);
- });
- });
- });
- ```
-
- **Key Points (Unit)**:
-
- - Pure function testing—no framework dependencies
- - Fast execution (milliseconds)
- - Edge case coverage (zero, negative, empty inputs)
- - High cyclomatic complexity handled at unit level
-
- ## When to Use Which Level
-
- | Scenario | Unit | Integration | E2E |
- | ---------------------- | ------------- | ----------------- | ------------- |
- | Pure business logic | ✅ Primary | ❌ Overkill | ❌ Overkill |
- | Database operations | ❌ Can't test | ✅ Primary | ❌ Overkill |
- | API contracts | ❌ Can't test | ✅ Primary | ⚠️ Supplement |
- | User journeys | ❌ Can't test | ❌ Can't test | ✅ Primary |
- | Component props/events | ✅ Partial | ⚠️ Component test | ❌ Overkill |
- | Visual regression | ❌ Can't test | ⚠️ Component test | ✅ Primary |
- | Error handling (logic) | ✅ Primary | ⚠️ Integration | ❌ Overkill |
- | Error handling (UI) | ❌ Partial | ⚠️ Component test | ✅ Primary |
-
- ## Anti-Pattern Examples
-
- **❌ BAD: E2E test for business logic**
-
- ```typescript
- // DON'T DO THIS
- test('calculate discount via UI', async ({ page }) => {
- await page.goto('/calculator');
- await page.fill('[data-testid="price"]', '100');
- await page.fill('[data-testid="discount"]', '20');
- await page.click('[data-testid="calculate"]');
- await expect(page.getByText('$80')).toBeVisible();
- });
- // Problem: Slow, brittle, tests logic that should be unit tested
- ```
-
- **✅ GOOD: Unit test for business logic**
-
- ```typescript
- test('calculate discount', () => {
- expect(calculateDiscount(100, 20)).toBe(80);
- });
- // Fast, reliable, isolated
- ```
-
- _Source: Murat Testing Philosophy (test pyramid), existing test-levels-framework.md structure._
|