Verdict
No
No, do not add unit tests for code whose results your integration tests already assert in CI; add unit tests only for logic with more input combinations than those tests run, such as date rules, and for code that computes money or decides access.
Why
- Blast radiususers
- Change frequencyregularly
- Detectabilityimmediately
- Reversibilitytrivial
- Test costmoderate
Do not add unit tests for code your integration tests already check. The typical case is a backend whose integration tests call each endpoint against a real database in CI and assert the response and the saved row, with a distinct value in each field. Blast radius is users and Change frequency is regularly, because endpoints change with most features. Detectability is immediately, because a change that breaks an asserted result fails CI before merge. Reversibility is trivial, because an unmerged change leaves nothing to repair, and Test cost is moderate, because a unit test needs a fake for every collaborator. Rule R8 gives Do not test; branches the integration tests skip move Detectability to eventually and Reversibility to with-effort, which gives Test.
| When | Decision | Why |
|---|---|---|
| The covered code calculates a refund or an invoice total | Test mandatory: unit tests for each rule and the rounding boundaries | Blast radius rises to money and Reversibility to costly |
| The covered code decides who may read another account's records | Test mandatory: one allowed and one denied case for each role | Blast radius rises to safety-or-legal and Detectability to never, because a leaked record raises no error |
| A function has more input combinations than the integration tests run, such as due dates across weekends | Test: a parametrized unit test with one row for each combination | Detectability moves to eventually and Reversibility to with-effort, because a wrong saved due date looks plausible |
| The integration tests check only the status code, or reuse one value across several fields | Test: assert the response body and the saved row, with a distinct value in each field | Detectability moves to eventually and Reversibility to with-effort, because a wrong saved value passes the test |
| A search endpoint returns an empty list on a search service timeout that no integration test forces | Test minimally: one unit test with a fake client that times out | Detectability moves to eventually, because an empty list looks like a search with no matches |
| A two-minute unit test would cover a formatting function, and the integration tests assert its output for every branch, each with a different result | Do not add a unit test for the formatting function | Test cost falls to trivial, but Detectability stays immediately and Reversibility trivial |
What breaks if you don't test
Failures get past integration tests in branches those tests never take. An endpoint test that opens a ticket on a Tuesday morning runs one path through the due-date rules. A ticket opened on Friday evening or in another time zone takes a path nobody ran, and its wrong due date is saved and looks plausible.
What you lose if you over-test
A unit test that repeats an integration test doubles the updates when a behaviour changes. A unit test that mocks the repository fails on refactors that change no response, and it cannot see the SQL or the column mapping, where integration tests find their bugs.
What to do instead
- Keep one integration test for each main path and each error response users see, with a distinct value in each field, at the integration level of the Practical Test Pyramid.
- Unit test the functions whose input combinations outnumber the integration tests: date rules, parsers and calculations. Jest
test.eachputs each combination in one row of a table. - Read the coverage report of the integration suite once, and unit test each branch with stakes that it never runs.
When the answer changes
- The integration tests check only the status code, or run nightly instead of on each pull request.
- A rule has many input combinations, and the integration tests run one of them.
- The code computes money or decides who may read a record.
Real incident + Code example
The Friday tickets no endpoint test opened
On a helpdesk product I worked on, the API tests checked the due date of one ticket: high priority, opened on a Tuesday morning. A refactor moved the business-hours calculation into the customer's time zone. The test stayed green, but tickets opened after 17:00 on a Friday got a Saturday due date. On Monday they showed as breached and jumped ahead of tickets about to breach. Three weeks later a team lead asked why the SLA report showed breaches on Saturdays, and a script recomputed due dates for about 700 open tickets. The fix was a table of unit tests for the due-date function:
import { dueAt } from "./sla";
// Business hours 09:00 to 17:00, Monday to Friday, customer's local time.
// High priority: 4 business hours. Normal: 8 business hours.
test.each([
["2026-03-10T10:00", "high", "2026-03-10T14:00"], // Tuesday, same day
["2026-03-10T15:00", "high", "2026-03-11T11:00"], // rolls into Wednesday
["2026-03-13T16:00", "high", "2026-03-16T12:00"], // Friday into Monday
["2026-03-13T20:00", "normal", "2026-03-16T17:00"], // after Friday hours
["2026-03-14T11:00", "high", "2026-03-16T13:00"], // opened on Saturday
["2026-03-06T16:00", "high", "2026-03-09T12:00"], // across the DST change
])("opened %s, %s priority, due %s", (opened, priority, due) => {
expect(dueAt(opened, priority, "America/New_York")).toBe(due);
});
Related questions
FAQ
- Should I cover code with unit tests if it is already covered by integration tests?
No, code whose results integration tests already assert in CI needs no second test at the unit level. Add unit tests for branches those tests never run, and for code that computes money or decides access.
- Why should I bother with unit testing if I can just use integration tests?
Unit tests pay off for logic with many input combinations, because each combination costs one table row instead of an endpoint test with its own database setup. For controllers and mappers, integration tests with a distinct value in each field are enough.
- Should I skip unit tests if integration tests cover the same scenarios?
Yes, skip unit tests that repeat scenarios your integration tests already assert, because a second copy doubles the updates when a behaviour changes. Keep unit tests for the scenarios the integration tests leave out.
- Can integration tests replace unit tests?
Integration tests can replace unit tests for code whose paths they all run and assert, such as a CRUD endpoint. For a function with many input combinations, a unit test table costs less than an endpoint test per combination.