They answer different questions

Unit tests and integration tests are often presented as competing approaches. They are not. Each gives us evidence about a different type of risk.

A unit test asks whether a small piece of behavior is correct in isolation. An integration test asks whether multiple real components work together across a boundary. A passing unit suite can prove that pricing rules are correct while missing a broken database mapping. A passing integration suite can prove that an endpoint stores an order while failing to explore every branch in a complex calculation.

The useful question is not which kind is better. It is which failure each test is designed to detect.

Reliable software needs quick, precise feedback while code is changing and realistic evidence before it reaches users. That is why both layers matter.

What unit tests do best

A good unit test executes one observable behavior with minimal infrastructure. It should be fast, deterministic, and easy to run on a developer machine. When it fails, the cause should be close to the assertion.

Unit tests are especially effective for business rules, calculations, validation, state transitions, parsers, mapping logic, authorization decisions, and edge cases. They let us exercise combinations that would be cumbersome to construct through a full application.

The benefits

  • Fast feedback: hundreds or thousands can run in seconds.
  • Precise diagnosis: a focused failure usually points directly to the behavior that changed.
  • Safe refactoring: tests protect public behavior while implementation details evolve.
  • Executable examples: well-named tests document rules and edge cases.
  • Broad scenario coverage: unusual inputs and error paths are inexpensive to explore.

Isolation must not become an obsession. Mocking every collaborator often creates tests that repeat the implementation rather than verify useful outcomes. Prefer real value objects and simple in-memory collaborators. Mock a boundary when controlling it makes the test clearer, faster, or deterministic.

What integration tests do best

Integration tests exercise a meaningful path through real components. Depending on the system, that may include the HTTP pipeline, dependency injection, authentication, serialization, database driver, migrations, message broker, filesystem, or an external service contract.

These tests catch failures caused by assumptions between parts: a column name that no longer matches, a JSON format that changed, a route that was not registered, a transaction that does not commit, or configuration that is valid in code but wrong at runtime.

The benefits

  • Configuration confidence: the application is assembled the way production expects.
  • Contract verification: serialization, schemas, protocols, and status codes are exercised together.
  • Infrastructure realism: queries and migrations run against an actual database engine.
  • Workflow evidence: important user or business paths work across layers.
  • Deployment protection: environment assumptions become visible before release.

Use production-compatible dependencies where behavior matters. SQLite is not a complete substitute for SQL Server or PostgreSQL. A hand-built fake broker will not reproduce Service Bus delivery semantics. Containers or isolated cloud resources can provide higher fidelity, but they require careful lifecycle management.

What each layer misses alone

A unit-only strategy can produce a comforting green build around components that cannot communicate. An integration-only strategy tends to be slow, hard to diagnose, and too expensive for exhaustive rule coverage. Teams then run it less often, which delays feedback and reduces its value.

The combination closes both gaps: unit tests explore behavior deeply, while integration tests verify that the real system honors the assumptions made at its boundaries.

Build a balanced test portfolio

Do not chase a universal ratio. The right balance depends on risk. A calculation library may be dominated by unit tests. A data integration service may need more database and contract tests. A thin CRUD application may get more value from API-level integration tests than from mocking every repository call.

  1. Identify critical behaviors. Start with money movement, permissions, data integrity, irreversible actions, and high-traffic workflows.
  2. Put detailed rules in unit tests. Cover the normal path, boundaries, invalid inputs, and important failure states.
  3. Test every important boundary. Verify databases, queues, HTTP contracts, identity, and serialization with realistic dependencies.
  4. Keep a small set of end-to-end journeys. Use them to prove that the deployed system works, not to duplicate every scenario.
  5. Remove redundant tests. More tests are not automatically more confidence if they all prove the same fact.

Coverage can reveal untested code, but a percentage is not evidence that the right behavior was tested. Treat coverage as a diagnostic tool rather than a target that rewards low-value assertions.

Design tests around observable behavior

A test should explain a rule in its name, arrange only the facts that matter, perform one meaningful action, and assert the outcome a caller can observe. Avoid asserting private method calls or the exact sequence of internal collaboration unless that sequence is itself part of the contract.

Tests must be deterministic. Control time through a clock abstraction, generate known identifiers, isolate shared state, and never depend on execution order. Integration tests should create their own data and clean it up, or start from a known disposable environment.

Use test data builders to keep setup readable. A builder can provide valid defaults while each test overrides only the value relevant to its scenario. This keeps the reason for the test visible and reduces maintenance when the domain model changes.

A test is production code for confidence. Give it the same attention to naming, duplication, boundaries, and maintainability as the system it protects.

Use speed to shape the delivery pipeline

Run unit tests on every local change and pull request. Run focused integration tests on each pull request as well, parallelizing by dependency or feature when the suite grows. Reserve expensive end-to-end and performance suites for appropriate pipeline stages without making them so infrequent that failures become archaeological work.

A useful pipeline gives developers the most specific failures first. Compile and static checks, then unit tests, then integration tests, then broader deployed-system checks. Fail fast, publish readable results, retain diagnostic logs, and quarantine a flaky test only with an owner and a deadline.

Flakiness is a defect. Re-running a failed suite until it passes hides nondeterminism and teaches the team to distrust red builds. Track flaky tests, find the shared state, timing assumption, or environment leak, and fix the source.

A practical checklist

  • Every important business rule has focused unit coverage.
  • Database tests use the same engine family as production.
  • External API contracts are verified at the boundary.
  • Tests do not depend on order, wall-clock timing, or shared mutable data.
  • Mocks represent true boundaries rather than every class interaction.
  • Failure messages make the broken behavior easy to identify.
  • Pull requests run both fast unit tests and focused integration tests.
  • Flaky tests are treated as defects with owners.
  • Coverage informs review but does not replace risk-based judgment.
  • Slow or redundant tests are simplified before developers learn to avoid the suite.

The purpose of testing is not to accumulate tests. It is to create timely, trustworthy evidence that the software behaves correctly. Unit tests provide depth and speed. Integration tests provide realism across boundaries. Together, they make change safer without asking either layer to prove what it cannot.

About Ashkan Parsa

Ashkan Parsa is a Vancouver-based software engineer and technology leader with more than 30 years of experience across payment systems, systems integration, cloud platforms, data, automation, and applied AI.

View profile and experience →