Skip to main content

Test Granularity: Unit vs Integration During a Refactor

What This Concept Is​

Test scope and test purpose are separate axes. A characterization test can exercise one function or a whole service. A unit test can use real collaborators; an integration test need not cross a process boundary. Runtime is measured, not defined by those labels.

Choose checks at the boundary affected by the move: a helper extraction needs enclosing public behavior checks; an adapter move needs wiring and compatibility evidence; a state migration needs interleaved old/new client tests. No test category guarantees coverage by itself.

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.

Passing tests alone does not authorize deleting the re-export. Confirm all supported consumers have migrated or retain the compatibility entry point under the published policy.

Common Confusion / Misconception​

Coverage reports show execution, not whether assertions detect a relevant fault. Seed a wrong boundary comparison or a disconnected adapter and check that the suite rejects it.

Integration tests are not inherently too slow for the inner loop. Measure the actual suite and use a focused set between steps, retaining wider checks before a checkpoint. A fast mock that encodes the same misunderstanding as the implementation is weak evidence.

How To Use It​

Map each preserved observation to a test boundary. Record fixture control, oracle independence and runtime. Include real adapter wiring when it is affected; do not add a new test tier solely because a file moved. Try one deliberate fault and retain the smallest useful failing case.

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.

Engineering Extension: Use faults to evaluate the safety net​

Change per-line rounding to aggregate rounding, split the grid into two stores, or retry a timed-out provider through another. A useful checker rejects the fault for a public observation. Passing seeded faults supports these cases only; it is not a universal completeness score.

Use the behavior contracts, labs and practice feedback to test the claim.

Read This Only If Stuck​

Locate this move through the Refactoring book index. The selective reading guide gives exact consulted local locators and primary documentation. Index links are not exact chapter links.