Universal Testing Strategies
Overview
Language-agnostic testing patterns and strategies applicable across all technology stacks.
Testing Pyramid
code
┌───────┐
│ E2E │ Few, slow, expensive
─┴───────┴─
┌───────────┐
│Integration│ Some, medium speed
─┴───────────┴─
┌───────────────┐
│ Unit │ Many, fast, cheap
─┴───────────────┴─
Unit Tests (70-80%)
- •Test individual functions/methods in isolation
- •Mock external dependencies
- •Fast execution (< 10ms per test)
- •High volume, run on every save
Integration Tests (15-20%)
- •Test component interactions
- •Real database/API calls (often with test containers)
- •Medium speed (100ms - 1s per test)
- •Run on commit/PR
End-to-End Tests (5-10%)
- •Test complete user flows
- •Real browser/environment
- •Slow (seconds to minutes)
- •Run before deployment
Test Structure: AAA Pattern
code
// Arrange - Set up test data and conditions
const user = createTestUser({ email: 'test@example.com' });
const service = new UserService(mockDb);
// Act - Execute the code under test
const result = await service.createUser(user);
// Assert - Verify the expected outcome
expect(result.id).toBeDefined();
expect(result.email).toBe('test@example.com');
Naming Conventions
Test File Names
code
component.test.ts # Unit tests component.spec.ts # Alternative convention component.integration.ts # Integration tests component.e2e.ts # End-to-end tests
Test Case Names
Use descriptive names that explain the scenario:
code
// Pattern: should [expected behavior] when [condition]
describe('UserService', () => {
describe('createUser', () => {
it('should create user when valid data provided', () => {});
it('should throw ValidationError when email is invalid', () => {});
it('should throw DuplicateError when email already exists', () => {});
});
});
BDD Style (Given-When-Then)
code
describe('User Registration', () => {
describe('given a valid email and password', () => {
describe('when the user submits the form', () => {
it('then creates a new account', () => {});
it('then sends a welcome email', () => {});
});
});
});
Mocking Strategies
Test Doubles Overview
| Type | Purpose | Example |
|---|---|---|
| Stub | Returns predetermined values | stub.returns(42) |
| Mock | Verifies interactions | expect(mock).toHaveBeenCalled() |
| Spy | Wraps real implementation | spy(realFunction) |
| Fake | Working implementation (simplified) | In-memory database |
| Dummy | Placeholder (not used) | Required parameter |
When to Mock
DO Mock:
- •External services (APIs, databases)
- •Time-dependent functions
- •Random number generators
- •File system operations
- •Network requests
DON'T Mock:
- •The code under test
- •Simple value objects
- •Pure functions with no side effects
Mock Example
typescript
// Mock external API
const mockApi = {
getUser: jest.fn().mockResolvedValue({ id: '1', name: 'Test' })
};
// Inject mock
const service = new UserService(mockApi);
const result = await service.getUser('1');
// Verify interaction
expect(mockApi.getUser).toHaveBeenCalledWith('1');
expect(result.name).toBe('Test');
Test Data Management
Test Factories
Create reusable factory functions for test data:
typescript
// factories/user.ts
export function createTestUser(overrides = {}) {
return {
id: randomUUID(),
email: `test-${Date.now()}@example.com`,
name: 'Test User',
createdAt: new Date(),
...overrides
};
}
// In tests
const user = createTestUser({ name: 'Custom Name' });
Fixtures
Static test data for consistent testing:
typescript
// fixtures/users.ts
export const validUser = {
email: 'valid@example.com',
password: 'SecurePass123!'
};
export const invalidEmails = [
'no-at-sign',
'@no-local.com',
'no-domain@',
'spaces in@email.com'
];
Assertion Best Practices
Be Specific
typescript
// BAD - vague assertion
expect(result).toBeTruthy();
// GOOD - specific assertion
expect(result.success).toBe(true);
expect(result.data.id).toBe('expected-id');
One Logical Assertion Per Test
typescript
// BAD - multiple unrelated assertions
it('should process order', () => {
expect(order.id).toBeDefined();
expect(order.total).toBe(100);
expect(emailService.send).toHaveBeenCalled();
expect(inventory.reduce).toHaveBeenCalled();
});
// GOOD - focused tests
it('should assign an order ID', () => {
expect(order.id).toBeDefined();
});
it('should calculate correct total', () => {
expect(order.total).toBe(100);
});
it('should send confirmation email', () => {
expect(emailService.send).toHaveBeenCalled();
});
Custom Matchers
Create domain-specific matchers for readability:
typescript
expect.extend({
toBeValidEmail(received) {
const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
return {
pass,
message: () => `expected ${received} to be a valid email`
};
}
});
// Usage
expect('user@example.com').toBeValidEmail();
Edge Cases to Test
Input Validation
- •Empty/null/undefined inputs
- •Boundary values (0, -1, MAX_INT)
- •Invalid types
- •Malformed data
State Transitions
- •Initial state
- •After single operation
- •After multiple operations
- •After error and recovery
Async Operations
- •Successful completion
- •Timeout
- •Network errors
- •Concurrent operations
- •Race conditions
Error Handling
- •Expected errors (validation)
- •Unexpected errors (system)
- •Error recovery
- •Error propagation
Code Coverage Guidelines
Coverage Targets
| Type | Target | Notes |
|---|---|---|
| Line Coverage | 80% | Minimum acceptable |
| Branch Coverage | 75% | Test all conditionals |
| Function Coverage | 90% | All public APIs |
| Critical Paths | 100% | Auth, payments, data integrity |
Coverage Pitfalls
High coverage ≠ good tests
typescript
// 100% coverage but useless test
it('should run without error', () => {
processOrder(order); // No assertions!
});
Focus on:
- •Business-critical paths
- •Edge cases and error handling
- •Integration points
Test Performance
Keep Tests Fast
| Test Type | Target Time |
|---|---|
| Unit test | < 10ms |
| Integration test | < 1s |
| E2E test | < 30s |
| Full suite | < 5min |
Parallelization
bash
# Run tests in parallel vitest --pool=threads jest --maxWorkers=4 pytest -n auto go test -parallel 4
Test Isolation
Tests should not depend on each other:
typescript
// BAD - shared state
let counter = 0;
it('test 1', () => { counter++; });
it('test 2', () => { expect(counter).toBe(1); }); // Fragile!
// GOOD - isolated state
beforeEach(() => { counter = 0; });
it('test 1', () => { counter++; expect(counter).toBe(1); });
it('test 2', () => { counter++; expect(counter).toBe(1); });
CI/CD Integration
Pre-commit
- •Lint checks
- •Type checks
- •Unit tests (fast)
Pull Request
- •Full unit test suite
- •Integration tests
- •Coverage report
Pre-deployment
- •E2E tests
- •Performance tests
- •Security scans
Test Maintenance
Avoid Flaky Tests
- •Don't depend on timing
- •Don't depend on external services
- •Use deterministic data
- •Retry with care (hide real issues)
Keep Tests Readable
- •Clear naming
- •Minimal setup
- •Obvious assertions
- •Helpful failure messages
Review Test Code
- •Test code is production code
- •Apply same quality standards
- •Refactor when needed
Testing strategies applicable to all technology stacks