Where Should Unit Tests End and Integration Tests Begin - Drawing the Boundary and a Practical Decision Table

· · Testing, Unit Testing, Integration Testing, Test Design, Windows Development, C#, .NET

The short answer

The boundary between test layers is decided not by where the code lives, but by what kind of uncertainty you want to reduce.

  1. Pure logic belongs in unit tests (tests of judgment)
  2. Connection, wiring, conversion, and environmental differences belong in integration tests (tests of connection)
  3. If either layer could verify it, prefer a unit test first
  4. Don’t make integration tests broad and heavy - keep their boundaries narrow

Decision table

What you want to verify Primary test layer Notes
Price calculation, discounts, state transitions, input validation Unit test You want to run many input patterns
Exception classification, error message selection, retry decisions Unit test The meaning is complete without real I/O
Repository SQL / ORM mapping, transactions Integration test Real DB or real provider behavior is the point
JSON / XML / CSV serialize / deserialize Integration test Format mismatches are hard to catch with fakes
Routing, model binding, filters Integration test Verifies the connection to the framework
ViewModel or Presenter state transitions Unit test Meaningful even without bringing up the UI
Real Binding, Dispatcher, control lifecycle Integration test Framework and threading behavior is the topic
File paths, permissions, locks, character encoding Integration test You need real OS and file system behavior
COM registration, 32-bit/64-bit, STA/MTA, DLL loading Integration test Environmental differences and process boundaries are the topic
App startup, end-to-end check of major use cases E2E / smoke A small number is enough

What belongs in unit tests

Unit tests are a good fit for responsibilities that still have meaning once you remove the outside world.

  • Business rules, branches, state transitions
  • Input validation, error classification
  • Decisions about retry policy
  • ViewModel / Presenter state changes

In particular, the more combinations involved, the more value there is in pushing it into a unit test. As branching conditions multiply, running all of them through integration tests gets heavy fast.

Iron rules for unit tests

  • Inject the current time
  • Make GUIDs and random numbers replaceable
  • Don’t wait with sleep
  • Don’t touch a real DB, real files, or the real network

When mocks pile up

If you end up with seven mocks and a setup block longer than the code under test, it usually means one of two things: the class under test has too many responsibilities, or you are forcing wiring that should be checked in an integration test into a unit test.

Four boundaries that push tests up to integration

1. Format boundary

JSON/XML/CSV, DB schema and mapping, nullable/precision/timezone, character encodings, BOMs, and line endings. Any boundary that involves serialize/deserialize is a candidate for integration testing.

2. Wiring boundary

DI registration, configuration binding, routing, model binding, filters, middleware, host startup, and event wiring. You are checking that multiple real parts are connected correctly.

3. Environment boundary

File permissions, shared folders, file locks, administrator privileges, COM registration, 32-bit/64-bit, STA/MTA, and where DLLs get loaded from. This is especially important on Windows.

4. Time boundary

Timeouts, cancellation, real retry behavior, timer-driven processing, stopping background work, race conditions, and shutdown ordering. The key is to separate “the decision” from “the actual behavior”.

  • How many times you retry -> unit test
  • Whether the timeout actually fires -> integration test

Common misjudgments

  1. Mocking the Repository and calling it done - SQL correctness and transactions can only be verified by integration tests
  2. Trying to cover the framework inside a Controller unit test - Routing and model binding belong on the integration side
  3. Trying to brute-force every input pattern in integration tests - Branch coverage belongs in unit tests; representative cases belong in integration tests
  4. Hitting production external services from CI - Use a local DB, a temporary directory, a test host, and a dedicated test environment
Layer Primary test type What goes here
Core Heavy on unit tests Business rules, state transitions, input validation, error classification
Boundary Narrow integration tests DB, files, HTTP, serializers, DI, configuration, COM, permissions
Whole A small set of smoke / E2E Startup checks, key flows, regression guards for major incidents

Unit tests get thick by count; integration tests get thick by density at the boundaries.

Five questions when in doubt

  1. If you replaced the dependency with an in-memory fake, would the test still mean something? -> If yes, it is a unit test
  2. When it breaks, would you suspect connection or configuration rather than logic? -> If yes, it is an integration test
  3. Is the topic DB / files / serializer / DI / route / OS / permissions / bitness / threads? -> If yes, integration test
  4. Do you want to run a large number of input patterns quickly? -> If yes, unit test
  5. When this test fails, do you immediately know what to fix? -> If not, you have mixed test layers

Summary

  • Unit tests are tests of judgment
  • Integration tests are tests of connection
  • Brute-forcing branches belongs in unit tests
  • Format, wiring, environment, and time belong in integration tests
  • End-to-end checks are a small set of smoke / E2E tests

The three things to avoid most are: feeling that mocks have proven you connect to the real thing, trying to cover every branch in integration tests, and mixing the responsibilities of different test layers. When you are unsure, first ask yourself: would the bug be a broken “judgment” or a broken “connection”?

References

Related Articles

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

Related Topics

These topic pages place the article in a broader service and decision context.

Where This Topic Connects

This article connects naturally to the following service pages.

Back to the Blog