Where Should Unit Tests End and Integration Tests Begin - Drawing the Boundary and a Practical Decision Table
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.
- Pure logic belongs in unit tests (tests of judgment)
- Connection, wiring, conversion, and environmental differences belong in integration tests (tests of connection)
- If either layer could verify it, prefer a unit test first
- 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
- Mocking the Repository and calling it done - SQL correctness and transactions can only be verified by integration tests
- Trying to cover the framework inside a Controller unit test - Routing and model binding belong on the integration side
- Trying to brute-force every input pattern in integration tests - Branch coverage belongs in unit tests; representative cases belong in integration tests
- Hitting production external services from CI - Use a local DB, a temporary directory, a test host, and a dedicated test environment
A recommended three-layer setup
| 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
- If you replaced the dependency with an in-memory fake, would the test still mean something? -> If yes, it is a unit test
- When it breaks, would you suspect connection or configuration rather than logic? -> If yes, it is an integration test
- Is the topic DB / files / serializer / DI / route / OS / permissions / bitness / threads? -> If yes, integration test
- Do you want to run a large number of input patterns quickly? -> If yes, unit test
- 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”?
Related articles
- A Minimum Security Checklist for Windows Application Development
- How Far Can a Windows App Really Be a Single Binary
- When Does a Windows App Actually Require Administrator Privileges
- What Is Reg-Free COM
References
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
When You Can't Avoid Building Your Own Logger: Practical Minimum Requirements and Integration Test Checks
When you have no choice but to build a custom application logger, here are the minimum requirements to lock down first and the integratio...
Serial Communication App Pitfalls - Sort Out 1-Byte Reads, Timeouts, Flow Control, Reconnects, USB Adapters, and UI Freezes Up Front
A practitioner-oriented guide to the points serial communication apps trip on — framing, multiple kinds of timeout, RTS/CTS and DTR, reco...
What to Check Before Migrating .NET Framework to .NET — A Practical Premigration Checklist
A practical checklist for what to clean up, what to peel off, and what to drop before you start a .NET Framework to .NET migration — cove...
How to Ship C# as a Native DLL with Native AOT - Calling UnmanagedCallersOnly Exports from C/C++
A practical guide to publishing a C# class library as a native DLL with Native AOT and calling it from C/C++ via UnmanagedCallersOnly — c...
Why Bring Generic Host / BackgroundService into a Desktop App - Startup, Lifetime, and Graceful Shutdown Get Much Easier to Reason About
If startup, shutdown, exception handling, and periodic work are starting to bleed into the UI of your WPF or WinForms resident app, this ...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Technical Consulting & Design Review
We help clarify design direction, architectural boundaries, lifetime ownership, and how to handle legacy Windows assets.