Complete rewrite of the senior-qa skill addressing all feedback from Issue #51: SKILL.md (444 lines): - Added proper YAML frontmatter with trigger phrases - Added Table of Contents - Focused on React/Next.js testing (Jest, RTL, Playwright) - 3 actionable workflows with numbered steps - Removed marketing language References (3 files, 2,625+ lines total): - testing_strategies.md: Test pyramid, coverage targets, CI/CD patterns - test_automation_patterns.md: Page Object Model, fixtures, mocking, async testing - qa_best_practices.md: Naming conventions, isolation, debugging strategies Scripts (3 files, 2,261+ lines total): - test_suite_generator.py: Scans React components, generates Jest+RTL tests - coverage_analyzer.py: Parses Istanbul/LCOV, identifies critical gaps - e2e_test_scaffolder.py: Scans Next.js routes, generates Playwright tests Documentation: - Updated engineering-team/README.md senior-qa section - Added README.md in senior-qa subfolder Resolves #51 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
17 KiB
Testing Strategies for React and Next.js Applications
Comprehensive guide to test architecture, coverage targets, and CI/CD integration patterns.
Table of Contents
- The Testing Pyramid
- Testing Types Deep Dive
- Coverage Targets and Thresholds
- Test Organization Patterns
- CI/CD Integration Strategies
- Testing Decision Framework
The Testing Pyramid
The testing pyramid guides how to distribute testing effort across different test types for optimal ROI.
Classic Pyramid Structure
/\
/ \ E2E Tests (5-10%)
/----\ - User journey validation
/ \ - Critical path coverage
/--------\ Integration Tests (20-30%)
/ \ - Component interactions
/ \ - API integration
/--------------\ Unit Tests (60-70%)
/ \ - Individual functions
------------------ - Isolated components
React/Next.js Adapted Pyramid
For frontend applications, the pyramid shifts slightly:
| Level | Percentage | Tools | Focus |
|---|---|---|---|
| Unit | 50-60% | Jest, RTL | Pure functions, hooks, isolated components |
| Integration | 25-35% | RTL, MSW | Component trees, API calls, context |
| E2E | 10-15% | Playwright | Critical user flows, cross-page navigation |
Why This Distribution?
Unit tests are fast and cheap:
- Execute in milliseconds
- Pinpoint failures precisely
- Easy to maintain
- Run on every commit
Integration tests balance coverage and cost:
- Test realistic scenarios
- Catch component interaction bugs
- Moderate execution time
- Run on every PR
E2E tests are expensive but essential:
- Validate real user experience
- Catch deployment issues
- Slow and brittle
- Run on staging/production
Testing Types Deep Dive
Unit Testing
Purpose: Verify individual units of code work correctly in isolation.
What to Unit Test:
- Pure utility functions
- Custom hooks (with renderHook)
- Individual component rendering
- State reducers
- Validation logic
- Data transformers
Example: Testing a Pure Function
// utils/formatPrice.ts
export function formatPrice(cents: number, currency = 'USD'): string {
const formatter = new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
});
return formatter.format(cents / 100);
}
// utils/formatPrice.test.ts
describe('formatPrice', () => {
it('formats cents to USD by default', () => {
expect(formatPrice(1999)).toBe('$19.99');
});
it('handles zero', () => {
expect(formatPrice(0)).toBe('$0.00');
});
it('supports different currencies', () => {
expect(formatPrice(1999, 'EUR')).toContain('€');
});
it('handles large numbers', () => {
expect(formatPrice(100000000)).toBe('$1,000,000.00');
});
});
Example: Testing a Custom Hook
// hooks/useCounter.ts
export function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset };
}
// hooks/useCounter.test.ts
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('starts with initial value', () => {
const { result } = renderHook(() => useCounter(5));
expect(result.current.count).toBe(5);
});
it('increments count', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.increment());
expect(result.current.count).toBe(1);
});
it('decrements count', () => {
const { result } = renderHook(() => useCounter(5));
act(() => result.current.decrement());
expect(result.current.count).toBe(4);
});
it('resets to initial value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => result.current.increment());
act(() => result.current.reset());
expect(result.current.count).toBe(10);
});
});
Integration Testing
Purpose: Verify multiple units work together correctly.
What to Integration Test:
- Component trees with multiple children
- Components with context providers
- Form submission flows
- API call and response handling
- State management interactions
- Router-dependent components
Example: Testing Component with API Call
// components/UserProfile.tsx
export function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, [userId]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user?.name}</div>;
}
// components/UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { UserProfile } from './UserProfile';
const server = setupServer(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.json({ id: req.params.id, name: 'John Doe' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('UserProfile', () => {
it('shows loading state initially', () => {
render(<UserProfile userId="123" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('displays user name after loading', async () => {
render(<UserProfile userId="123" />);
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
it('displays error on API failure', async () => {
server.use(
rest.get('/api/users/:id', (req, res, ctx) => {
return res(ctx.status(500));
})
);
render(<UserProfile userId="123" />);
await waitFor(() => {
expect(screen.getByText(/Error/)).toBeInTheDocument();
});
});
});
End-to-End Testing
Purpose: Verify complete user flows work in a real browser environment.
What to E2E Test:
- Critical business flows (checkout, signup, login)
- Cross-page navigation sequences
- Authentication flows
- Third-party integrations
- Payment processing
- Form wizards
Example: Testing Checkout Flow
// e2e/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('completes purchase successfully', async ({ page }) => {
// Add product to cart
await page.goto('/products/widget-pro');
await page.getByRole('button', { name: 'Add to Cart' }).click();
// Verify cart updated
await expect(page.getByTestId('cart-count')).toHaveText('1');
// Go to checkout
await page.getByRole('link', { name: 'Checkout' }).click();
// Fill shipping info
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Address').fill('123 Test St');
await page.getByLabel('City').fill('Test City');
await page.getByLabel('Zip').fill('12345');
// Fill payment info (test card)
await page.getByLabel('Card Number').fill('4242424242424242');
await page.getByLabel('Expiry').fill('12/25');
await page.getByLabel('CVC').fill('123');
// Submit order
await page.getByRole('button', { name: 'Place Order' }).click();
// Verify confirmation
await expect(page).toHaveURL(/\/orders\/\w+/);
await expect(page.getByText('Order Confirmed')).toBeVisible();
});
test('shows validation errors for invalid input', async ({ page }) => {
await page.goto('/checkout');
await page.getByRole('button', { name: 'Place Order' }).click();
await expect(page.getByText('Email is required')).toBeVisible();
await expect(page.getByText('Address is required')).toBeVisible();
});
});
Visual Regression Testing
Purpose: Catch unintended visual changes to UI components.
Tools: Playwright visual comparisons, Percy, Chromatic
Example: Visual Snapshot Test
// e2e/visual/components.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('button variants render correctly', async ({ page }) => {
await page.goto('/storybook/button');
await expect(page).toHaveScreenshot('button-variants.png');
});
test('responsive header', async ({ page }) => {
// Desktop
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('/');
await expect(page.locator('header')).toHaveScreenshot('header-desktop.png');
// Mobile
await page.setViewportSize({ width: 375, height: 667 });
await expect(page.locator('header')).toHaveScreenshot('header-mobile.png');
});
});
Accessibility Testing
Purpose: Ensure application is usable by people with disabilities.
Tools: jest-axe, @axe-core/playwright
Example: Automated A11y Testing
// Unit/Integration level with jest-axe
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from './Button';
expect.extend(toHaveNoViolations);
describe('Button accessibility', () => {
it('has no accessibility violations', async () => {
const { container } = render(<Button>Click me</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
// E2E level with Playwright + Axe
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('homepage has no a11y violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
Coverage Targets and Thresholds
Recommended Thresholds by Project Type
| Project Type | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
| Startup/MVP | 60% | 50% | 60% | 60% |
| Growing Product | 75% | 70% | 75% | 75% |
| Enterprise | 85% | 80% | 85% | 85% |
| Safety Critical | 95% | 90% | 95% | 95% |
Coverage by Code Type
High Coverage Priority (80%+):
- Business logic
- State management
- API handlers
- Form validation
- Authentication/authorization
- Payment processing
Medium Coverage Priority (60-80%):
- UI components
- Utility functions
- Data transformers
- Custom hooks
Lower Coverage Priority (40-60%):
- Static pages
- Simple wrappers
- Configuration files
- Types/interfaces
Jest Coverage Configuration
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.stories.{ts,tsx}',
'!src/**/index.{ts,tsx}', // barrel files
'!src/types/**',
],
coverageThreshold: {
global: {
statements: 80,
branches: 75,
functions: 80,
lines: 80,
},
// Higher thresholds for critical paths
'./src/services/payment/': {
statements: 95,
branches: 90,
functions: 95,
lines: 95,
},
'./src/services/auth/': {
statements: 90,
branches: 85,
functions: 90,
lines: 90,
},
},
coverageReporters: ['text', 'lcov', 'html', 'json'],
};
Test Organization Patterns
Co-located Tests (Recommended for React)
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # Unit tests
│ │ ├── Button.stories.tsx # Storybook
│ │ └── index.ts
│ └── Form/
│ ├── Form.tsx
│ ├── Form.test.tsx
│ └── Form.integration.test.tsx # Integration tests
├── hooks/
│ ├── useAuth.ts
│ └── useAuth.test.ts
└── utils/
├── formatters.ts
└── formatters.test.ts
Separate Test Directory
src/
├── components/
├── hooks/
└── utils/
__tests__/
├── unit/
│ ├── components/
│ ├── hooks/
│ └── utils/
├── integration/
│ └── flows/
└── fixtures/
├── users.json
└── products.json
e2e/
├── specs/
│ ├── auth.spec.ts
│ └── checkout.spec.ts
├── fixtures/
│ └── auth.ts
└── pages/ # Page Object Models
├── LoginPage.ts
└── CheckoutPage.ts
Test File Naming Conventions
| Pattern | Use Case |
|---|---|
*.test.ts |
Unit tests |
*.spec.ts |
Integration/E2E tests |
*.integration.test.ts |
Explicit integration tests |
*.e2e.spec.ts |
Explicit E2E tests |
*.a11y.test.ts |
Accessibility tests |
*.visual.spec.ts |
Visual regression tests |
CI/CD Integration Strategies
Pipeline Stages
# .github/workflows/test.yml
name: Test Pipeline
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
jobs:
unit:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage
- uses: codecov/codecov-action@v4
with:
files: coverage/lcov.info
fail_ci_if_error: true
integration:
name: Integration Tests
runs-on: ubuntu-latest
needs: unit
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:integration
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: integration
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npm run test:e2e
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Test Splitting for Speed
# Run E2E tests in parallel across multiple machines
e2e:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
PR Gating Rules
| Test Type | When to Run | Block Merge? |
|---|---|---|
| Unit | Every commit | Yes |
| Integration | Every PR | Yes |
| E2E (smoke) | Every PR | Yes |
| E2E (full) | Merge to main | No (alert only) |
| Visual | Every PR | No (review required) |
| Performance | Weekly/Release | No (alert only) |
Testing Decision Framework
When to Write Which Test
Is it a pure function with no side effects?
├── Yes → Unit test
└── No
├── Does it make API calls or use context?
│ ├── Yes → Integration test with mocking
│ └── No
│ ├── Is it a critical user flow?
│ │ ├── Yes → E2E test
│ │ └── No → Integration test
└── Is it UI-focused with many visual states?
├── Yes → Storybook + Visual test
└── No → Component unit test
Test ROI Matrix
| Test Type | Write Time | Run Time | Maintenance | Confidence |
|---|---|---|---|---|
| Unit | Low | Very Fast | Low | Medium |
| Integration | Medium | Fast | Medium | High |
| E2E | High | Slow | High | Very High |
| Visual | Low | Medium | Medium | High (UI) |
When NOT to Test
- Generated code (GraphQL types, Prisma client)
- Third-party library internals
- Implementation details (internal state, private methods)
- Simple pass-through wrappers
- Type definitions
Red Flags in Testing Strategy
| Red Flag | Problem | Solution |
|---|---|---|
| E2E tests > 30% | Slow CI, flaky tests | Push logic down to integration |
| Only unit tests | Missing interaction bugs | Add integration tests |
| Testing mocks | Not testing real behavior | Test behavior, not implementation |
| 100% coverage goal | Diminishing returns | Focus on critical paths |
| No E2E tests | Missing deployment issues | Add smoke tests for critical flows |
Summary
- Follow the pyramid: 60% unit, 30% integration, 10% E2E
- Set thresholds by risk: Higher coverage for critical paths
- Co-locate tests: Keep tests close to source code
- Automate in CI: Run tests on every PR, gate merges on failure
- Decide wisely: Not everything needs every type of test