Test Levels Guide
Explains test types & guides test selection using car analogy.
When to Use
Invoke when:
- Explaining test concepts to team members
- Deciding which test type to write
- Reviewing test coverage strategy
- Onboarding new developers to testing
The 3 Test Levels
1. Unit Test (Test Case)
The specific instruction.
Single fn/component tested in isolation. No external deps (DB, API, browser).
| Aspect | Description |
|--------|-------------|
| Analogy | "Check if left turn signal blinks when I push lever down" |
| Scope | Tiny detail - one fn, one input/output |
| Speed | Fast (ms) |
| Location | tests/unit/ |
When to write:
- Pure functions (formatters, validators, utils)
- Data transformations
- Business logic w/o side effects
Project example:
// tests/unit/utils/formatters.spec.ts
import {expect, test} from 'next/experimental/testmode/playwright';
import {leadingZero, formatCurrency} from '@/app/_utils/formatters';
test.describe('formatters', () => {
test('leadingZero adds zero to single-digit numbers', () => {
expect(leadingZero(7)).toBe('07');
expect(leadingZero(10)).toBe('10');
});
test('formatCurrency formats correctly', () => {
const result = formatCurrency({value: 1234.56, locale: 'en', currency: 'USD'});
expect(result).toBe('1,234.56 $');
});
});
2. Integration Test
The handshake.
Tests if 2+ parts communicate correctly. Focuses on connections, not full system.
| Aspect | Description |
|--------|-------------|
| Analogy | "Does engine make wheels turn?" (Engine → Transmission) |
| Scope | Connections between components |
| Speed | Medium (100ms-few seconds) |
| Location | tests/integration/ |
When to write:
- Validators w/ schemas (Zod)
- API route handlers
- Service-to-service communication
- DB queries w/ mocked data
Project example:
// tests/integration/validators/offer.spec.ts
import {expect, test} from 'next/experimental/testmode/playwright';
import {mockLoggedUser} from '../../common';
import {validateCustomerStatus} from '@/app/_lib/validator';
test.beforeEach(async ({context}) => {
await mockLoggedUser(context);
});
test.describe('validate customer status', () => {
const validData = {
offerId: '670e80f0a65da593d265088a',
status: 'viewing',
};
test('returns success for valid data', async () => {
const result = validateCustomerStatus(validData);
expect(result.success).toBe(true);
expect(result.data).toEqual(validData);
});
test('returns error for invalid offerId', async () => {
const result = validateCustomerStatus({...validData, offerId: ''});
expect(result.success).toBe(false);
expect(result.error?.issues[0]?.path).toContain('offerId');
});
});
3. E2E Test (End-to-End)
The real user journey.
Full system test: browser, DB, network, 3rd-party services. Exactly as user experiences.
| Aspect | Description |
|--------|-------------|
| Analogy | "Start car, drive to store, park, turn off" |
| Scope | Full user flow |
| Speed | Slow (seconds-minutes) |
| Location | tests/pages/ |
When to write:
- Critical user flows (login, checkout, payment)
- Multi-page journeys
- Features requiring browser interaction
- Smoke tests for deployment
Project example:
// tests/pages/start.spec.ts
import {expect, test} from 'next/experimental/testmode/playwright';
import {mockLoggedUser, resetAPIEndpointsMock} from '../common';
test.beforeEach(async ({context}) => {
await mockLoggedUser(context);
});
test.afterEach(async ({next, context}) => {
await resetAPIEndpointsMock(next);
await context.clearCookies();
});
test('start page renders correctly', async ({page}) => {
test.setTimeout(120000);
await page.goto('http://localhost:3000/start', {
timeout: 90000,
waitUntil: 'domcontentloaded'
});
await expect(page).toHaveURL('http://localhost:3000/start');
await expect(page.getByTestId('pageHeader')).toHaveClass('drop-shadow-font');
await expect(page.getByTestId('subTotal')).toContainText('Sub total');
await expect(page.getByTestId('startCounter')).toContainText('Your vacation starts in');
});
Quick Decision Guide
| Question | Test Type | |----------|-----------| | "Does this fn return correct value?" | Unit | | "Do these 2 parts work together?" | Integration | | "Does full flow work for user?" | E2E |
Test Pyramid
/\
/E2E\ Few (slow, expensive)
/------\
/Integr- \ Some (medium)
/ ation \
/------------\
/ Unit \ Many (fast, cheap)
/________________\
Rule: More unit tests, fewer E2E tests. Unit tests catch bugs early & run fast.
Project Structure
tests/
├── unit/ # Pure fn tests (no browser)
│ └── utils/ # Utility fn tests
├── integration/ # Component interaction tests
│ ├── validators/ # Schema validation tests
│ └── lib/ # Library fn tests
├── pages/ # E2E browser tests
│ ├── start.spec.ts
│ ├── payment.spec.ts
│ └── confirm.spec.ts
├── common.ts # Shared test utilities
├── mock/ # Mock data & helpers
└── seed.spec.ts # DB seed for tests
Commands
# Run all tests (headed mode)
npm run test
# Run specific test file
npm run test tests/unit/utils/formatters.spec.ts
# Run tests in UI mode
npm run test:ui
# Show test report
npm run test:report
Best Practices
- Name tests clearly:
should [action] when [condition] - One assertion focus: Test one behavior per test
- Use test.describe: Group related tests
- Clean up: Use
afterEachfor state reset - Mock external deps: Use
mockLoggedUser,resetAPIEndpointsMock - Set timeouts: E2E tests need longer timeouts (120s)
- Use data-testid: For reliable element selection
Summary Table
| Level | Question | Scope | Speed | Location |
|-------|----------|-------|-------|----------|
| Unit | "Does this fn work?" | Single fn | Fast | tests/unit/ |
| Integration | "Do parts connect?" | Connections | Medium | tests/integration/ |
| E2E | "Does flow work?" | Full system | Slow | tests/pages/ |