10 min read • Guide 605 of 877
Test Strategy Best Practices
A solid test strategy catches bugs early, enables confident refactoring, and prevents regressions from reaching production. The testing pyramid—unit tests at the base, integration in the middle, E2E at the top—provides the right balance of speed and coverage. GitScrum helps teams track testing tasks alongside development work, ensuring quality is built into the process, not bolted on at the end.
Test Types
| Type | Speed | Scope | Count |
|---|---|---|---|
| Unit | Fast | Single function/class | Many |
| Integration | Medium | Component connections | Some |
| E2E | Slow | Full user flows | Few |
| Contract | Fast | API boundaries | As needed |
| Performance | Slow | Load/stress | Key paths |
Testing Pyramid
TEST PYRAMID STRUCTURE
PYRAMID SHAPE:
┌─────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ │
│ │ E2E │ Few, slow, critical │
│ ┌──┴─────────┴──┐ │
│ │ Integration │ Some, moderate │
│ ┌──┴───────────────┴──┐ │
│ │ Unit │ Many, fast │
│ └─────────────────────┘ │
│ │
│ Good balance: │
│ ├── 70% unit tests │
│ ├── 20% integration tests │
│ └── 10% E2E tests │
└─────────────────────────────────────────────────┘
ANTI-PATTERN - ICE CREAM CONE:
┌─────────────────────────────────────────────────┐
│ ┌─────────────────────┐ │
│ │ Manual testing │ │
│ └───────┬─────────────┘ │
│ ┌────┴────┐ │
│ │ E2E │ Too many E2E │
│ └────┬────┘ │
│ ┌─────┴─────┐ │
│ │Integration│ │
│ └─────┬─────┘ │
│ ┌───┴───┐ │
│ │ Unit │ Too few unit │
│ └───────┘ │
│ │
│ Problems: Slow CI, flaky tests, long feedback │
└─────────────────────────────────────────────────┘
Unit Testing
UNIT TEST STRATEGY
WHAT TO UNIT TEST:
┌─────────────────────────────────────────────────┐
│ High priority: │
│ ├── Business logic │
│ ├── Complex calculations │
│ ├── State transitions │
│ ├── Edge cases │
│ ├── Error handling │
│ └── Utility functions │
│ │
│ Low priority: │
│ ├── Simple getters/setters │
│ ├── Framework boilerplate │
│ └── Third-party library wrappers │
└─────────────────────────────────────────────────┘
UNIT TEST PRINCIPLES:
┌─────────────────────────────────────────────────┐
│ FIRST: │
│ ├── Fast: Milliseconds, not seconds │
│ ├── Isolated: No external dependencies │
│ ├── Repeatable: Same result every time │
│ ├── Self-validating: Pass/fail clear │
│ └── Timely: Written with or before code │
│ │
│ AAA Pattern: │
│ ├── Arrange: Set up test data │
│ ├── Act: Execute the function │
│ └── Assert: Verify the result │
└─────────────────────────────────────────────────┘
GOOD UNIT TEST EXAMPLE:
┌─────────────────────────────────────────────────┐
│ describe('calculateDiscount', () => { │
│ it('applies 10% discount for orders > $100', │
│ () => { │
│ // Arrange │
│ const order = { total: 150 }; │
│ │
│ // Act │
│ const result = calculateDiscount(order); │
│ │
│ // Assert │
│ expect(result).toBe(15); │
│ }); │
│ │
│ it('returns 0 for orders <= $100', () => { │
│ const order = { total: 100 }; │
│ expect(calculateDiscount(order)).toBe(0); │
│ }); │
│ }); │
└─────────────────────────────────────────────────┘
Integration Testing
INTEGRATION TEST STRATEGY
WHAT TO INTEGRATION TEST:
┌─────────────────────────────────────────────────┐
│ ├── Database queries and transactions │
│ ├── API endpoints │
│ ├── Service-to-service communication │
│ ├── Message queue producers/consumers │
│ ├── External API integrations │
│ └── Cache interactions │
│ │
│ Focus on: │
│ ├── Happy path works end-to-end │
│ ├── Error handling at boundaries │
│ └── Data flows correctly between components │
└─────────────────────────────────────────────────┘
INTEGRATION TEST APPROACH:
┌─────────────────────────────────────────────────┐
│ Database tests: │
│ ├── Use test database (not mock) │
│ ├── Reset data between tests │
│ ├── Use transactions for cleanup │
│ └── Consider testcontainers │
│ │
│ API tests: │
│ ├── Test real HTTP calls │
│ ├── Verify response format │
│ ├── Check status codes │
│ └── Test auth/authorization │
│ │
│ External services: │
│ ├── Use mocks for third-party APIs │
│ ├── Or use sandbox environments │
│ └── Test contract separately │
└─────────────────────────────────────────────────┘
API TEST EXAMPLE:
┌─────────────────────────────────────────────────┐
│ describe('POST /api/orders', () => { │
│ it('creates order and returns 201', async () │
│ => { │
│ const response = await request(app) │
│ .post('/api/orders') │
│ .send({ product: 'widget', qty: 2 }) │
│ .set('Authorization', 'Bearer token'); │
│ │
│ expect(response.status).toBe(201); │
│ expect(response.body.id).toBeDefined(); │
│ │
│ // Verify in database │
│ const order = await db.orders │
│ .findById(response.body.id); │
│ expect(order.product).toBe('widget'); │
│ }); │
│ }); │
└─────────────────────────────────────────────────┘
E2E Testing
E2E TEST STRATEGY
WHAT TO E2E TEST:
┌─────────────────────────────────────────────────┐
│ Test critical user journeys: │
│ ├── Sign up and onboarding │
│ ├── Core feature happy paths │
│ ├── Purchase/checkout flow │
│ ├── Authentication flow │
│ └── Critical error recovery │
│ │
│ Limit scope: │
│ ├── 10-20 critical scenarios │
│ ├── Not every permutation │
│ └── Cover what other tests can't │
└─────────────────────────────────────────────────┘
E2E TEST PRINCIPLES:
┌─────────────────────────────────────────────────┐
│ ├── Test user behavior, not implementation │
│ ├── Use stable selectors (data-testid) │
│ ├── Handle async properly (wait for elements) │
│ ├── Keep tests independent │
│ ├── Clean up test data │
│ └── Run in isolated environment │
└─────────────────────────────────────────────────┘
FIGHTING FLAKINESS:
┌─────────────────────────────────────────────────┐
│ Common causes and fixes: │
│ │
│ Timing issues: │
│ ├── Use explicit waits for elements │
│ ├── Wait for network requests to complete │
│ └── Don't use arbitrary sleeps │
│ │
│ Data issues: │
│ ├── Create fresh test data each run │
│ ├── Clean up after tests │
│ └── Use unique identifiers │
│ │
│ Environment issues: │
│ ├── Consistent test environment │
│ ├── Control external dependencies │
│ └── Retry flaky tests with investigation │
└─────────────────────────────────────────────────┘
CI/CD Integration
TESTS IN CI/CD PIPELINE
PIPELINE STRATEGY:
┌─────────────────────────────────────────────────┐
│ On every commit (< 5 min): │
│ ├── Linting │
│ ├── Type checking │
│ └── Unit tests │
│ │
│ On PR (< 15 min): │
│ ├── Unit tests │
│ ├── Integration tests │
│ └── Build verification │
│ │
│ Pre-deploy (< 30 min): │
│ ├── Full test suite │
│ └── E2E tests │
│ │
│ Nightly/scheduled: │
│ ├── Full E2E suite │
│ ├── Performance tests │
│ └── Security scans │
└─────────────────────────────────────────────────┘
OPTIMIZING CI TIME:
┌─────────────────────────────────────────────────┐
│ Parallelization: │
│ ├── Run test suites in parallel │
│ ├── Shard large test suites │
│ └── Use multiple CI runners │
│ │
│ Smart selection: │
│ ├── Run only affected tests on commits │
│ ├── Full suite on main branch │
│ └── Use dependency graph for selection │
│ │
│ Caching: │
│ ├── Cache node_modules │
│ ├── Cache Docker layers │
│ └── Cache test database setup │
└─────────────────────────────────────────────────┘
Coverage Strategy
TEST COVERAGE APPROACH
COVERAGE TARGETS:
┌─────────────────────────────────────────────────┐
│ Overall target: 70-80% │
│ │
│ Differentiated targets: │
│ ├── Core business logic: 90%+ │
│ ├── API handlers: 80% │
│ ├── Utilities: 70% │
│ └── UI components: 60-70% │
│ │
│ Exclude from coverage: │
│ ├── Generated code │
│ ├── Configuration files │
│ ├── Type definitions │
│ └── Third-party wrappers │
└─────────────────────────────────────────────────┘
MEANINGFUL COVERAGE:
┌─────────────────────────────────────────────────┐
│ ✓ Test behavior, not lines │
│ ✓ Cover edge cases and error paths │
│ ✓ Test critical paths thoroughly │
│ ✓ Test code that has caused bugs │
│ │
│ ✗ Just hit lines for metrics │
│ ✗ Test implementation details │
│ ✗ Ignore error handling │
│ ✗ Skip complex code because it's hard │
└─────────────────────────────────────────────────┘
COVERAGE ENFORCEMENT:
┌─────────────────────────────────────────────────┐
│ In CI: │
│ ├── Fail if coverage drops below threshold │
│ ├── Report coverage on PRs │
│ └── Track coverage trends over time │
│ │
│ In code review: │
│ ├── New code must include tests │
│ ├── Check tests are meaningful │
│ └── Coverage for critical changes │
└─────────────────────────────────────────────────┘
Maintenance
TEST MAINTENANCE
AVOIDING TEST DEBT:
┌─────────────────────────────────────────────────┐
│ Write maintainable tests: │
│ ├── Clear test names that describe behavior │
│ ├── DRY with test utilities │
│ ├── Avoid testing implementation details │
│ ├── Use page objects for E2E │
│ └── Keep tests simple and readable │
│ │
│ Test naming: │
│ ✓ "should return empty array for no matches" │
│ ✗ "test1" or "returns correctly" │
└─────────────────────────────────────────────────┘
DEALING WITH FLAKY TESTS:
┌─────────────────────────────────────────────────┐
│ 1. Quarantine: Move to separate suite │
│ 2. Investigate: Find root cause │
│ 3. Fix or remove: Don't leave broken │
│ 4. Track: Monitor flakiness rates │
│ │
│ Flaky test policy: │
│ ├── Fails 2x in a week → investigate │
│ ├── Can't fix in 2 days → quarantine │
│ └── In quarantine > 1 week → delete │
└─────────────────────────────────────────────────┘
TEST REFACTORING:
┌─────────────────────────────────────────────────┐
│ When to refactor tests: │
│ ├── Tests are slow (minutes for unit) │
│ ├── Tests are brittle (break on refactor) │
│ ├── Tests are hard to understand │
│ ├── Duplicated test setup │
│ └── Can't tell what failed from test name │
└─────────────────────────────────────────────────┘
Best Practices
- Follow the pyramid — many unit, fewer integration, few E2E
- Test behavior — not implementation details
- Keep tests fast — optimize CI pipeline
- Fight flakiness — quarantine and fix quickly
- Measure coverage meaningfully, not just lines
- Maintain tests — treat as production code
- Automate in CI — tests run on every change
- Write readable tests — documentation of behavior
Anti-Patterns
✗ Ice cream cone (too many E2E)
✗ Testing implementation details
✗ Ignoring flaky tests
✗ Chasing coverage numbers only
✗ No tests in CI
✗ Tests slower than development