Testing Strategy in Practice: From Unit Tests to Integration Tests
Testing is not about "testing is better than not testing" — it is about "what to test, how to test, and how much to test." This article covers the test pyramid, responsibilities of unit tests, integration tests, and E2E tests, coverage strategies, and how to make testing part of the development workflow — for teams building or optimizing their testing practice.
The Bottom Line: Testing Is Not About “Did We Test” — It Is About “Did We Test the Right Things”
Many teams treat testing as a “pre-deployment security check,” cutting test time to meet deadlines, or blindly chasing coverage metrics. Both approaches miss the point — testing is part of the development workflow, not a gate.
This article covers the test pyramid and practical approaches for each layer.
1. The Test Pyramid
/\
/ \ E2E tests (few, critical paths)
/ \
/------\
/Integration\ (key path coverage)
/------------\
/ Unit Tests \ (many, fast, core logic)
/----------------\
Layer Responsibilities
| Layer | Speed | Quantity | Responsibility |
|---|---|---|---|
| Unit tests | Milliseconds | Many | Verify individual function/class behavior |
| Integration tests | Seconds | Medium | Verify inter-module interaction |
| E2E tests | Minutes | Few | Verify critical user flows |
2. Unit Tests
What to Test
- Business logic (calculations, validation, transformations)
- Edge cases (nulls, out-of-bounds, invalid input)
- Error handling (does the function return the correct error?)
What NOT to Test
- Framework behavior (Express routing, React rendering — already tested by the framework)
- Database queries (that is integration testing)
- Third-party API calls (mock them, only verify call parameters and response handling)
Example
function calculateDiscount(amount: number, level: 'bronze' | 'silver' | 'gold'): number {
const rates = { bronze: 0, silver: 0.1, gold: 0.2 };
return amount * (1 - rates[level]);
}
describe('calculateDiscount', () => {
it('returns full amount for bronze', () => {
expect(calculateDiscount(100, 'bronze')).toBe(100);
});
it('applies 10% discount for silver', () => {
expect(calculateDiscount(100, 'silver')).toBe(90);
});
it('handles zero amount', () => {
expect(calculateDiscount(0, 'gold')).toBe(0);
});
});
3. Integration Tests
What to Test
- API request and response handling
- Database read/write operations
- Service-to-service message passing
Configuration
beforeAll(async () => {
await db.migrate.latest();
await db.seed.run();
});
afterAll(async () => {
await db.destroy();
});
describe('POST /users', () => {
it('creates a new user', async () => {
const res = await request(app)
.post('/users')
.send({ name: 'test', email: 'test@test.com' });
expect(res.status).toBe(201);
expect(res.body.name).toBe('test');
});
});
4. E2E Tests
What to Test
- Critical user flows (registration, login, checkout)
- Cross-system interactions (frontend → API → database → third-party)
Principles
- Test only critical paths, not all paths
- Use Cypress or Playwright
- Run daily in CI, or before merging to main
5. Testing Strategy Summary
| Test Type | Coverage Target | Frequency | Fail Allowed? |
|---|---|---|---|
| Unit tests | Core logic 80%+ | Every commit | No |
| Integration tests | Key API paths | Daily | No |
| E2E tests | Critical user flows | Before merge | Yes (requires manual review) |
Testing maturity is not measured by “how many tests you have” — it is measured by “how naturally testing is integrated into your development workflow.” The ideal state: writing tests feels as natural as writing code, and not writing tests feels wrong.
Need testing system design or technical consulting? Contact us — tell us about your project scale and tech stack, feasibility within 24 hours.
Related reading
- Code Review in Practice — testing and review form a quality assurance system
- How to Run a Technical Review — upstream decision framework for testing strategy
- LLM Application Evaluation in Practice — the bridge between traditional testing and LLM evaluation: deterministic parts use assertions, probabilistic parts use eval sets
FAQ
What code coverage should we aim for?
Do not chase 100% coverage. Core logic (business rules, data processing, API endpoints) should target 80%+ coverage; UI layer and glue code can be at 50%. The key is not the coverage number but whether critical paths are tested. A project with 60% coverage but all core logic tested is better than 90% coverage with core logic untested.
Where is the line between unit tests and integration tests?
Unit tests do not depend on external systems (database, API, file system) — all external dependencies are mocked. Integration tests verify inter-module interactions with real external systems (test database, test API). The dividing line: if your test needs to start a database, it is an integration test, not a unit test.
What if tests are too slow?
Run them in layers: unit tests on every commit (seconds), integration tests daily (minutes), E2E tests before merging to main (tens of minutes). Slow tests are not eliminated — they are just not blocking the development flow.
Is TDD actually useful?
Yes, but it requires practice. The core value of TDD is not "write tests before code" — it is "think about expected behavior before writing code." If you are new to TDD, start with bug fixes: write a test that reproduces the bug, then fix the code, then the test passes. This gives you confidence and ensures the bug never returns.
This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?
Subscribe to Updates
Get notified when new articles are published. No spam, occasional updates only.
Subscribe →