Skip to main content

Test Granularity: Unit vs Integration During a Refactor

What This Concept Is

Tests exist at several scales. During a refactor, two matter most:

  • Unit tests: small, in-process, fast (sub-millisecond each). They exercise one function or class through its public API, with collaborators either real-and-trivial or stubbed.
  • Integration / component tests: cross-module, often cross-process. They cost tens or hundreds of milliseconds each. They exercise a slice of the system end-to-end within a bounded context.

Different refactor moves need different guardrails:

  • Intra-function moves (Extract Variable, Extract Function, Rename, Slide Statements): protected by unit tests of the enclosing function.
  • Cross-function moves (Move Function, Move Field, Change Function Declaration): protected by unit tests of both the source and the target, plus an integration test if the move crosses a module boundary.
  • Structural reshaping (Split Phase, Replace Conditional with Polymorphism): protected by a small number of integration tests first, with unit tests added as the new structure emerges.

Connection to primary concept (Characterization Tests): characterization tests are usually integration-level when you first meet legacy code, then migrate to unit-level after you have carved out the seams.

Why It Matters Here

If you protect everything with integration tests, your refactor loop is slow and coarse. If you protect everything with unit tests, a cross-module refactor may pass every unit test while breaking the wire between modules. Granularity matters.

The pragmatic rule: a refactor move's safety net is valid only if at least one test would fail if the move broke observable behavior at the right scope.

Concrete Example

You are about to Move Function parseOrder from module orders.js to module parsing.js.

  • Unit test in orders.test.js calls orders.parseOrder(...) -> still imports from orders.js, which now re-exports.
  • Unit test in parsing.test.js (new) directly tests the moved function in its new home.
  • Integration test checkoutFlow.test.js (existing) exercises the path HTTP -> orders -> parsing -> pricing -> DB and will catch wiring errors introduced by the move.

After the move, unit + integration both pass. Only then delete the re-export from orders.js.

Common Confusion / Misconception

"100% unit coverage = safe to refactor anything." No. Unit tests typically stub the seams, so they cannot catch a wiring bug introduced by Move Function or by changing a module's export surface. You need at least one integration test along the path the refactor crosses.

Conversely: "I have integration tests, I don't need unit tests to refactor." Integration tests are too slow to run after every Extract Function. You will either batch your changes (losing small-step discipline) or stop running tests (losing behavior preservation).

How To Use It

Check Yourself

  1. Why might a Move Function refactor pass all unit tests while breaking the app?
  2. If your integration tests take 3 minutes, why should you not rely on them during the inner loop of Extract Function?
  3. Which level of test is most useful when characterizing legacy code you have never seen?

Mini Drill or Application

Take a cross-module refactor you have done or are planning. List each test level that must stay green. For each, estimate its runtime. If your total runtime between refactor steps is over 10 seconds, plan one unit test you could add that would shrink the loop.

Video and Lecture References

Article References

External Exercises

  • Run your full test suite and record: how many tests run in less than 10ms, 10-100ms, 100ms-1s, >1s. Graph the shape.
  • Pick a slow integration test and propose one unit-level test that could replace part of its coverage.

Depth Path

  • Read This Only If Stuck - Fowler chunk 032 (Value of Self-Testing Code)
  • Optional deep dive: Kent Beck, Test-Driven Development: By Example, on test-size choices

Source Backbone

Refactoring is the canonical book backbone for this module. Use these sources after attempting the refactor and tests yourself.