Java Legacy Code Testing
Purpose
java-refactoring step 1 states "no net, no refactoring" and asks for characterisation tests
first. This skill exists for the situation where you cannot: new OrderProcessor() opens a
database connection, the method you must change reads LocalDate.now() and a static singleton,
and the class is final. There is no test to write yet, and the change that would make one
possible is itself untested.
Feathers's definition is the working one — "To me, legacy code is simply code without tests" —
and
his answer is a category of refactoring meant to be performed before any test exists:
"The refactorings in Dependency-Breaking Techniques are special in that they are meant to be
done without tests, in the service of putting tests in place." — Feathers, p. xxi
This skill owns that step and stops the moment the code is callable. What you do with the net
once you have it is java-refactoring.
Scope
Covers: the seam model and enabling points in Java, the dependency-breaking catalogue, Sprout
and Wrap, the disciplines that substitute for a test while you make the seam, and what in
2004-era legacy-testing advice is now dead.
Does not cover, and routes to: writing the characterisation tests themselves — method,
worked example, pinning non-return-value dimensions, when it is not worth it
(java-refactoring/references/safety-workflow.md) · the refactoring catalogue you apply once
tests exist (java-refactoring) · strangler, anti-corruption layer, decommissioning and
characterising at a system boundary (legacy-enterprise-modernization) · which double to use and
mockStatic policy (java-test-doubles) · which level to test at (java-testing-strategy) ·
test naming and structure (java-test-design) · the red-green-refactor loop (tdd) · whether
the debt is worth repaying at all (technical-debt-decisions).
Workflow
Examples compile for Java 21 without preview; applying the skill does not require upgrading a
legacy project. Inspect its release/toolchain, test framework, mock maker, runtime and build
constraints first. Reuse compatible tooling and report missing evidence rather than importing
the version table into the build. Prefer an already reachable boundary before editing a seam.
Feathers's Legacy Code Change Algorithm (ch. 2, p. 18), unchanged since 2004 because nothing has
superseded it:
- Identify change points. Where must the behaviour differ? No change point, no work — see
Over-application.
- Find test points. Where can you observe the effect of the change? Not necessarily where
you make it — if a coarser point is already reachable, pin there first and proceed. The
deadlock below is only about the narrow test you cannot yet reach.
references/seams-and-interception.md covers effect analysis and choosing the
narrowest interception point.
- Break dependencies. Only enough to reach step 4. This is the catalogue below.
- Write the tests. Characterisation, by running the code and recording what it does —
mechanics in
java-refactoring/references/safety-workflow.md.
- Make the change and refactor. Now the net exists;
java-refactoring takes over.
Steps 3 and 4 are the ones people invert, and inverting them is the deadlock this skill resolves.
Seam and enabling point
"A seam is a place where you can alter behavior in your program without editing in that place."
"Every seam has an enabling point, a place where you can make the decision to use one behavior
or another."
— Feathers, ch. 4; corroborated via Fowler, bliki/LegacySeam.html
The distinction is the single most useful thing the vocabulary buys, because it diagnoses the
commonest failed attempt: Extract Interface is applied, PaymentGateway now has one
implementation and one mock — but the class under test still calls new PaymentGatewayImpl()
internally. The seam exists and the enabling point does not. An interface whose only non-test
implementor is constructed with new inside its own consumer has bought nothing.
Feathers names three kinds. In Java one of them is unavailable and a fourth has appeared, and the
ordering below is not taste:
| Seam |
Enabling point |
Verdict |
| Object seam |
the constructor call in the composition root |
The default. Visible in source, and already where the application wires itself |
| Link seam |
the test classpath — shadowing, ServiceLoader, JPMS |
Last resort: no enabling point exists in the source, so a reader cannot see the swap |
| Bytecode instrumentation |
the try-with-resources around mockStatic |
Technically a seam, invisible from production code — it removes the pressure that would have fixed the design (java-test-doubles) |
Java has no preprocessing seam, and annotation processing is not a substitute
(references/seams-and-interception.md).
Choosing the technique
The obstacle decides, not the technique's popularity. Full catalogue with preconditions and
costs in references/dependency-breaking-catalogue.md; read it before applying anything below.
| The obstacle |
Technique |
The constructor news a collaborator that does I/O |
Parameterize Constructor |
| The method creates the object it depends on |
Parameterize Method |
| A static or singleton is read mid-method |
Introduce Instance Delegator or Encapsulate Global References |
The collaborator's type is untestable (HttpServletRequest, a vendor SDK type) |
Adapt Parameter |
| Only a subset of a fat class's methods are needed |
Extract Interface — of that subset only |
| The good name belongs to the class, not the interface |
Extract Implementer |
| One awkward call inside an otherwise reachable method |
Extract and Override Call |
| Logic that touches no instance state, in an unconstructible class |
Expose Static Method |
| A long method whose locals are hopelessly entangled |
Break Out Method Object |
Nothing works and the class is final with a private constructor |
Reconsider Sprout below |
Each technique's precondition and cost is in the catalogue reference; do not apply one without
reading its cost. Time is the exception that is not in the table: LocalDate.now() and
Instant.now() are answered by injecting a java.time.Clock, and how to do that is
java-test-design (references/determinism.md) and java-test-doubles — this skill only records
that Feathers's hand-rolled time abstraction is dead.
What you may change before a test exists
This is where the repo's own rule needs qualifying rather than repeating. java-refactoring's
"no net, no refactoring" is correct for category 2 below and is a deadlock for category 1.
- Small changes with a reviewable preservation argument, applied only to create a seam: Parameterize
Constructor with a delegating old constructor, Extract Interface, Extract Method, Rename —
preferably tool-assisted, existing caller contracts preserved, revertible as one small diff.
Compilation and unchanged signatures are evidence, not proof of unchanged behavior: compare
construction order, exception timing, virtual dispatch, reflection/DI and resource ownership.
- Everything else — reordering statements, merging branches, changing an extraction point,
altering a condition. These need the pinned suite first, no exceptions.
For category 1, Feathers's four disciplines (ch. 23) substitute for the test you cannot yet have:
- Preserve Signatures. Change no signature during the step. If nothing a caller can see
changed, cut-and-paste is verifiable by eye. This is why the delegating old constructor matters.
- Lean on the Compiler. Make the change that forces a compile error at every site that must
move, then fix them. Deliberately breaking compilation is a search tool, not an accident.
- Single-Goal Editing. One goal per editing session. "While I'm in here" is how a category-1
step becomes a category-2 change with no net.
- Hyperaware Editing. Know why every keystroke is safe. If you cannot say why, stop.
State the category and preservation argument in the change summary (and commit message if a
commit is requested). If it cannot be justified, first find a coarser test point or narrow the seam.
Before and after
Run scripts/renewal-service/verify.sh when you need to show a sceptic that the obstacle is real
rather than stylistic — it asserts that the before state cannot be constructed at all.
// Before — two obstacles, needing two different techniques.
final class RenewalCheck {
private final RateGateway rates = new RateGateway(); // 1: connects at construction
List<String> due(List<Policy> policies) {
LocalDate today = LocalDate.now(); // 2: hidden global
...
}
}
The first line of any test — new RenewalCheck() — throws before an assertion is reached:
measured, IllegalStateException: RateGateway: cannot connect to policy-db. And LocalDate.now()
leaves the assertion with no stable expected value, so a test that passes today and fails in
thirty days is worse than no test at all.
The after state applies Extract Interface under a new name — interface Rates, with
RateGateway implements Rates keeping the name it already had — and Parameterize Constructor,
retaining a delegating RenewalCheck() that passes new RateGateway() and
Clock.systemDefaultZone(). Both halves are Preserve Signatures: no caller of RateGateway and no
caller of RenewalCheck needs to move. Still compare initialization and dispatch behavior.
systemDefaultZone, not systemUTC, preserves the chosen local zone if the process default
zone stays stable. The clock captures that zone at construction while LocalDate.now() reads
the default on each call: if the application changes it dynamically, preserve that behavior
explicitly. Remove the old constructor only after public, reflective and DI callers are migrated.
Naming the interface RateGateway and renaming the class would have been Extract Implementer
(p. 356) instead — a different technique with a different cost, because every new RateGateway()
in the codebase then stops compiling.
The test then needs no mocking framework: a lambda stub for Rates, a fixed clock, and the answer
is [P1] on every machine on every date — measured. mockStatic(LocalDate.class) also makes that
test pass, and leaves the design exactly as broken.
When there is no time: Sprout and Wrap
You must add behaviour to a 600-line method by Thursday. Reading it is a week you do not have.
- Sprout Method / Sprout Class — write the new behaviour as a new, fully tested unit and call
it from one line inside the untested body. Read the surrounding control flow and effects to
prove placement, frequency, ordering and error propagation; testing the sprout alone cannot.
- Wrap Method / Wrap Class — rename the original, give the new method the old name, and have
it call both. Use when the new behaviour must happen around the old rather than inside it.
Say the honest thing when you use these: they buy safety for the new code and leave the legacy
body exactly as untested as it was, plus one more seam in a class that already had too many
shapes. They answer "I have two days"; they do not answer "how do we fix this".
Over-application
- An interface for every collaborator. Extract Interface applied uniformly produces forty
Thing/ThingImpl pairs, each with one implementation and one mock. Navigation doubles, the
interface accretes the whole class surface and documents nothing, and the mocks now let every
test lie about a collaborator that was never the risk. Feathers's own version is narrower:
extract the subset the client actually uses. An interface with one permanent implementation
can still protect an owned boundary; require an actual substitution or contract benefit
rather than counting implementations (java-dependency-inversion).
- A
Clock everywhere. Every constructor gains a Clock because "inject the clock" became a
rule; classes that never read the time carry a field they ignore and every test constructs a
clock it does not use. Inject it where now() is called. Nowhere else.
- Characterising code that is about to be deleted.
java-refactoring already excludes this;
the corollary this skill adds is the replacement case. If the code is being replaced rather
than deleted, characterise at the boundary that survives — the HTTP contract, the batch output — which is
legacy-enterprise-modernization's move, not this skill's.
- A harness around genuinely stable code. A tax-table lookup untouched for six years with no
pending change and no incidents. The algorithm starts at "identify change points": no change
point, no work. Coverage of stable code is a metric, not a benefit.
- Seams for their own sake. Every
new becomes a factory method, every static a delegator,
in code nobody is changing — indirection with a testing justification attached.
Rules
- Break dependencies only as far as step 4 needs. The refactoring you want to do is a separate
commit, after the tests exist.
- Preserve existing caller signatures where possible through overloads/delegation. Techniques
that deliberately change a signature or class identity need a caller/compatibility inventory
and the narrowest reachable checks; the compiler cannot find reflective or external callers.
- A seam without an enabling point is not a seam. Before declaring one done, name the line that
chooses the behaviour.
- Do not add
setXxxForTest, a public mutable static, or a protected factory method without a
ticket to remove it. These leak into production and become permanent mutable globals — the one
place an ArchUnit rule earns its keep here is failing the build when production code reaches one
(references/tooling-and-modernization.md).
mockStatic is the last option, not the first. It is a real seam with an invisible enabling
point, so it removes exactly the pressure that would have fixed the dependency
(java-test-doubles). If you use it, ticket the wrap.
- Reject any advice built on PowerMock (last release 2020, pins Mockito 3.3.3, documents
JDK ≤ 9) or on adding
mockito-inline (the inline mock maker has been Mockito's default
since 5.0.0; the artifact is frozen at 5.2.0). Both dominate the online corpus for this topic.
- "We'll write the tests after the refactor" ends with unfamiliar untested code, which is
strictly worse than the familiar untested code you started with.
Verification
- The chosen test point is reachable without the real side effect that blocked it. Often
this means constructing the class without a database; a static or coarser seam can also meet
the objective. Show the first meaningful assertion, not just successful construction.
- The enabling point is a line you can point at. If nobody can name it, the seam is decorative.
- The test fails when the behaviour changes. The mutation check is
java-refactoring/references/safety-workflow.md's; run it here too, because a suite that stays
green through a deliberate mutation means step 4 did not happen.
- The step is one reviewable, reversible diff. Inspect signatures plus construction order,
exceptions, dispatch and resource ownership; do not infer preservation from compilation alone.
grep -c 'mockStatic(' is not rising across the module, and no identifier matching
*ForTest* is reachable from production code.
- Time is injected, not frozen by luck.
grep for LocalDate.now(), Instant.now() and
System.currentTimeMillis() in the class you just covered.
Review prompts
- What exactly stops this class from being constructed in a test? Name the line.
- Where is the enabling point for this seam, and can a reader of the class see it?
- Is this change behaviour-preserving by construction, or does it need the net first?
- Did any signature change in this commit? If so, why was that necessary now?
- Is this
protected method, test-only setter or interface going to be deleted, and when?
- Are we pinning behaviour we are about to delete?
- What is the change point? If there is none, why are we here?
References
- Dependency-breaking catalogue — read before
applying any technique: every Java-relevant entry from Feathers ch. 25 plus Sprout and Wrap
from ch. 6, each with its precondition, its cost, the modern Java caveat, and the two Feathers
himself disliked.
- Seams and interception points — read when the question
is where to put the test rather than how to reach the code: the seam taxonomy in Java detail,
what replaces preprocessing seams, effect analysis, and choosing a pinch point so one test
covers a cluster of changes.
scripts/renewal-service/ — Before.java, After.java and
verify.sh. Run it when someone argues the obstacle is a matter of taste: the before state
throws at construction, the after state is deterministic, and the script fails if that stops
being true.
- Tooling and the 2004 to 2026 sweep — read when
following older legacy-testing material, or when the output is too large to assert on: verified
library coordinates, approval testing with scrubbers and a CI-safe reporter, and the table of
what in Feathers's mechanics has been superseded and by what.
1---2name: java-legacy-code-testing3description: Getting Java code under test before you change it, when you cannot construct the class or reach the method at all: seams and their enabling points, the dependency-breaking catalogue (Parameterize Constructor, Extract Interface, Extract and Override, Introduce Instance Delegator, Break Out Method Object, Expose Static Method), Sprout and Wrap when there is no time, approval testing when the output to pin is too large to assert on, and the disciplines that make a change safe while no test exists. Use when a constructor opens a connection, when a method reads a static singleton, when a test would need the real database, when mockStatic is proposed, when a setXxxForTest is being added, or when 2004-era advice (PowerMock, mockito-inline) is followed. Does not cover characterisation-test mechanics (java-refactoring), strangler work (legacy-enterprise-modernization), doubles (java-test-doubles), test level (java-testing-strategy), the red-green-refactor loop (tdd), or how a test is written (java-test-design).4---56# Java Legacy Code Testing78## Purpose910`java-refactoring` step 1 states "no net, no refactoring" and asks for characterisation tests11first. This skill exists for the situation where you cannot: `new OrderProcessor()` opens a12database connection, the method you must change reads `LocalDate.now()` and a static singleton,13and the class is `final`. There is no test to write yet, and the change that would make one14possible is itself untested.1516Feathers's definition is the working one — _"To me, legacy code is simply code without tests"_ —17and18his answer is a category of refactoring meant to be performed **before** any test exists:1920> "The refactorings in _Dependency-Breaking Techniques_ are special in that they are meant to be21> done **without tests**, in the service of putting tests in place." — Feathers, p. xxi2223This skill owns that step and stops the moment the code is callable. What you do with the net24once you have it is `java-refactoring`.2526## Scope2728**Covers:** the seam model and enabling points in Java, the dependency-breaking catalogue, Sprout29and Wrap, the disciplines that substitute for a test while you make the seam, and what in302004-era legacy-testing advice is now dead.3132**Does not cover, and routes to:** writing the characterisation tests themselves — method,33worked example, pinning non-return-value dimensions, when it is not worth it34(`java-refactoring/references/safety-workflow.md`) · the refactoring catalogue you apply once35tests exist (`java-refactoring`) · strangler, anti-corruption layer, decommissioning and36characterising at a system boundary (`legacy-enterprise-modernization`) · which double to use and37`mockStatic` policy (`java-test-doubles`) · which level to test at (`java-testing-strategy`) ·38test naming and structure (`java-test-design`) · the red-green-refactor loop (`tdd`) · whether39the debt is worth repaying at all (`technical-debt-decisions`).4041## Workflow4243Examples compile for Java 21 without preview; applying the skill does not require upgrading a44legacy project. Inspect its release/toolchain, test framework, mock maker, runtime and build45constraints first. Reuse compatible tooling and report missing evidence rather than importing46the version table into the build. Prefer an already reachable boundary before editing a seam.4748Feathers's Legacy Code Change Algorithm (ch. 2, p. 18), unchanged since 2004 because nothing has49superseded it:50511. **Identify change points.** Where must the behaviour differ? No change point, no work — see52 Over-application.532. **Find test points.** Where can you observe the effect of the change? Not necessarily where54 you make it — if a coarser point is already reachable, pin there first and proceed. The55 deadlock below is only about the narrow test you cannot yet reach. `references/seams-and-interception.md` covers effect analysis and choosing the56 narrowest interception point.573. **Break dependencies.** Only enough to reach step 4. This is the catalogue below.584. **Write the tests.** Characterisation, by running the code and recording what it does —59 mechanics in `java-refactoring/references/safety-workflow.md`.605. **Make the change and refactor.** Now the net exists; `java-refactoring` takes over.6162Steps 3 and 4 are the ones people invert, and inverting them is the deadlock this skill resolves.6364## Seam and enabling point6566> "A seam is a place where you can alter behavior in your program without editing in that place."67>68> "Every seam has an enabling point, a place where you can make the decision to use one behavior69> or another."70>71> — Feathers, ch. 4; corroborated via Fowler, `bliki/LegacySeam.html`7273The distinction is the single most useful thing the vocabulary buys, because it diagnoses the74commonest failed attempt: `Extract Interface` is applied, `PaymentGateway` now has one75implementation and one mock — but the class under test still calls `new PaymentGatewayImpl()`76internally. **The seam exists and the enabling point does not.** An interface whose only non-test77implementor is constructed with `new` inside its own consumer has bought nothing.7879Feathers names three kinds. In Java one of them is unavailable and a fourth has appeared, and the80ordering below is not taste:8182| Seam | Enabling point | Verdict |83| ---------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |84| **Object seam** | the constructor call in the composition root | **The default.** Visible in source, and already where the application wires itself |85| **Link seam** | the test classpath — shadowing, `ServiceLoader`, JPMS | Last resort: no enabling point exists _in the source_, so a reader cannot see the swap |86| **Bytecode instrumentation** | the `try`-with-resources around `mockStatic` | Technically a seam, invisible from production code — it removes the pressure that would have fixed the design (`java-test-doubles`) |8788Java has **no preprocessing seam**, and annotation processing is not a substitute89(`references/seams-and-interception.md`).9091## Choosing the technique9293The obstacle decides, not the technique's popularity. Full catalogue with preconditions and94costs in `references/dependency-breaking-catalogue.md`; read it before applying anything below.9596| The obstacle | Technique |97| ------------------------------------------------------------------------------- | --------------------------------------------------------------------- |98| The constructor `new`s a collaborator that does I/O | **Parameterize Constructor** |99| The method creates the object it depends on | **Parameterize Method** |100| A static or singleton is read mid-method | **Introduce Instance Delegator** or **Encapsulate Global References** |101| The collaborator's type is untestable (`HttpServletRequest`, a vendor SDK type) | **Adapt Parameter** |102| Only a subset of a fat class's methods are needed | **Extract Interface** — of that subset only |103| The good name belongs to the class, not the interface | **Extract Implementer** |104| One awkward call inside an otherwise reachable method | **Extract and Override Call** |105| Logic that touches no instance state, in an unconstructible class | **Expose Static Method** |106| A long method whose locals are hopelessly entangled | **Break Out Method Object** |107| Nothing works and the class is `final` with a `private` constructor | Reconsider **Sprout** below |108109Each technique's precondition and cost is in the catalogue reference; do not apply one without110reading its cost. Time is the exception that is not in the table: `LocalDate.now()` and111`Instant.now()` are answered by injecting a `java.time.Clock`, and how to do that is112`java-test-design` (`references/determinism.md`) and `java-test-doubles` — this skill only records113that Feathers's hand-rolled time abstraction is dead.114115## What you may change before a test exists116117This is where the repo's own rule needs qualifying rather than repeating. `java-refactoring`'s118"no net, no refactoring" is correct for **category 2** below and is a deadlock for **category 1**.1191201. **Small changes with a reviewable preservation argument**, applied only to create a seam: Parameterize121 Constructor with a delegating old constructor, Extract Interface, Extract Method, Rename —122 preferably tool-assisted, existing caller contracts preserved, revertible as one small diff.123 Compilation and unchanged signatures are evidence, not proof of unchanged behavior: compare124 construction order, exception timing, virtual dispatch, reflection/DI and resource ownership.1252. **Everything else** — reordering statements, merging branches, changing an extraction point,126 altering a condition. These need the pinned suite first, no exceptions.127128For category 1, Feathers's four disciplines (ch. 23) substitute for the test you cannot yet have:129130- **Preserve Signatures.** Change no signature during the step. If nothing a caller can see131 changed, cut-and-paste is verifiable by eye. This is why the delegating old constructor matters.132- **Lean on the Compiler.** Make the change that _forces_ a compile error at every site that must133 move, then fix them. Deliberately breaking compilation is a search tool, not an accident.134- **Single-Goal Editing.** One goal per editing session. "While I'm in here" is how a category-1135 step becomes a category-2 change with no net.136- **Hyperaware Editing.** Know why every keystroke is safe. If you cannot say why, stop.137138State the category and preservation argument in the change summary (and commit message if a139commit is requested). If it cannot be justified, first find a coarser test point or narrow the seam.140141## Before and after142143Run `scripts/renewal-service/verify.sh` when you need to show a sceptic that the obstacle is real144rather than stylistic — it asserts that the before state cannot be constructed at all.145146```java147// Before — two obstacles, needing two different techniques.148final class RenewalCheck {149 private final RateGateway rates = new RateGateway(); // 1: connects at construction150 List<String> due(List<Policy> policies) {151 LocalDate today = LocalDate.now(); // 2: hidden global152 ...153 }154}155```156157The first line of any test — `new RenewalCheck()` — throws before an assertion is reached:158measured, `IllegalStateException: RateGateway: cannot connect to policy-db`. And `LocalDate.now()`159leaves the assertion with no stable expected value, so a test that passes today and fails in160thirty days is worse than no test at all.161162The after state applies **Extract Interface** under a _new_ name — `interface Rates`, with163`RateGateway implements Rates` keeping the name it already had — and **Parameterize Constructor**,164retaining a delegating `RenewalCheck()` that passes `new RateGateway()` and165`Clock.systemDefaultZone()`. Both halves are Preserve Signatures: no caller of `RateGateway` and no166caller of `RenewalCheck` needs to move. Still compare initialization and dispatch behavior.167`systemDefaultZone`, not `systemUTC`, preserves the chosen local zone if the process default168zone stays stable. The clock captures that zone at construction while `LocalDate.now()` reads169the default on each call: if the application changes it dynamically, preserve that behavior170explicitly. Remove the old constructor only after public, reflective and DI callers are migrated.171172Naming the interface `RateGateway` and renaming the class would have been **Extract Implementer**173(p. 356) instead — a different technique with a different cost, because every `new RateGateway()`174in the codebase then stops compiling.175176The test then needs no mocking framework: a lambda stub for `Rates`, a fixed clock, and the answer177is `[P1]` on every machine on every date — measured. `mockStatic(LocalDate.class)` also makes that178test pass, and leaves the design exactly as broken.179180## When there is no time: Sprout and Wrap181182You must add behaviour to a 600-line method by Thursday. Reading it is a week you do not have.183184- **Sprout Method / Sprout Class** — write the new behaviour as a new, fully tested unit and call185 it from one line inside the untested body. Read the surrounding control flow and effects to186 prove placement, frequency, ordering and error propagation; testing the sprout alone cannot.187- **Wrap Method / Wrap Class** — rename the original, give the new method the old name, and have188 it call both. Use when the new behaviour must happen _around_ the old rather than inside it.189190Say the honest thing when you use these: they buy safety for the **new** code and leave the legacy191body exactly as untested as it was, plus one more seam in a class that already had too many192shapes. They answer "I have two days"; they do not answer "how do we fix this".193194## Over-application195196- **An interface for every collaborator.** Extract Interface applied uniformly produces forty197 `Thing`/`ThingImpl` pairs, each with one implementation and one mock. Navigation doubles, the198 interface accretes the whole class surface and documents nothing, and the mocks now let every199 test lie about a collaborator that was never the risk. Feathers's own version is narrower:200 extract **the subset the client actually uses**. An interface with one permanent implementation201 can still protect an owned boundary; require an actual substitution or contract benefit202 rather than counting implementations (`java-dependency-inversion`).203- **A `Clock` everywhere.** Every constructor gains a `Clock` because "inject the clock" became a204 rule; classes that never read the time carry a field they ignore and every test constructs a205 clock it does not use. Inject it where `now()` is called. Nowhere else.206- **Characterising code that is about to be deleted.** `java-refactoring` already excludes this;207 the corollary this skill adds is the replacement case. If the code is being _replaced_ rather208 than deleted, characterise at the boundary that survives — the HTTP contract, the batch output — which is209 `legacy-enterprise-modernization`'s move, not this skill's.210- **A harness around genuinely stable code.** A tax-table lookup untouched for six years with no211 pending change and no incidents. The algorithm starts at "identify change points": no change212 point, no work. Coverage of stable code is a metric, not a benefit.213- **Seams for their own sake.** Every `new` becomes a factory method, every static a delegator,214 in code nobody is changing — indirection with a testing justification attached.215216## Rules217218- Break dependencies only as far as step 4 needs. The refactoring you want to do is a separate219 commit, after the tests exist.220- Preserve existing caller signatures where possible through overloads/delegation. Techniques221 that deliberately change a signature or class identity need a caller/compatibility inventory222 and the narrowest reachable checks; the compiler cannot find reflective or external callers.223- A seam without an enabling point is not a seam. Before declaring one done, name the line that224 chooses the behaviour.225- Do not add `setXxxForTest`, a public mutable static, or a `protected` factory method without a226 ticket to remove it. These leak into production and become permanent mutable globals — the one227 place an ArchUnit rule earns its keep here is failing the build when production code reaches one228 (`references/tooling-and-modernization.md`).229- `mockStatic` is the last option, not the first. It is a real seam with an invisible enabling230 point, so it removes exactly the pressure that would have fixed the dependency231 (`java-test-doubles`). If you use it, ticket the wrap.232- Reject any advice built on **PowerMock** (last release 2020, pins Mockito 3.3.3, documents233 JDK ≤ 9) or on adding **`mockito-inline`** (the inline mock maker has been Mockito's default234 since 5.0.0; the artifact is frozen at 5.2.0). Both dominate the online corpus for this topic.235- "We'll write the tests after the refactor" ends with unfamiliar untested code, which is236 strictly worse than the familiar untested code you started with.237238## Verification239240- **The chosen test point is reachable without the real side effect that blocked it.** Often241 this means constructing the class without a database; a static or coarser seam can also meet242 the objective. Show the first meaningful assertion, not just successful construction.243- **The enabling point is a line you can point at.** If nobody can name it, the seam is decorative.244- **The test fails when the behaviour changes.** The mutation check is245 `java-refactoring/references/safety-workflow.md`'s; run it here too, because a suite that stays246 green through a deliberate mutation means step 4 did not happen.247- **The step is one reviewable, reversible diff.** Inspect signatures plus construction order,248 exceptions, dispatch and resource ownership; do not infer preservation from compilation alone.249- **`grep -c 'mockStatic('` is not rising** across the module, and no identifier matching250 `*ForTest*` is reachable from production code.251- **Time is injected, not frozen by luck.** `grep` for `LocalDate.now()`, `Instant.now()` and252 `System.currentTimeMillis()` in the class you just covered.253254## Review prompts255256- What exactly stops this class from being constructed in a test? Name the line.257- Where is the enabling point for this seam, and can a reader of the class see it?258- Is this change behaviour-preserving by construction, or does it need the net first?259- Did any signature change in this commit? If so, why was that necessary now?260- Is this `protected` method, test-only setter or interface going to be deleted, and when?261- Are we pinning behaviour we are about to delete?262- What is the change point? If there is none, why are we here?263264## References265266- [Dependency-breaking catalogue](references/dependency-breaking-catalogue.md) — read before267 applying any technique: every Java-relevant entry from Feathers ch. 25 plus Sprout and Wrap268 from ch. 6, each with its precondition, its cost, the modern Java caveat, and the two Feathers269 himself disliked.270- [Seams and interception points](references/seams-and-interception.md) — read when the question271 is _where_ to put the test rather than how to reach the code: the seam taxonomy in Java detail,272 what replaces preprocessing seams, effect analysis, and choosing a pinch point so one test273 covers a cluster of changes.274- [`scripts/renewal-service/`](scripts/renewal-service/) — `Before.java`, `After.java` and275 `verify.sh`. Run it when someone argues the obstacle is a matter of taste: the before state276 throws at construction, the after state is deterministic, and the script fails if that stops277 being true.278- [Tooling and the 2004 to 2026 sweep](references/tooling-and-modernization.md) — read when279 following older legacy-testing material, or when the output is too large to assert on: verified280 library coordinates, approval testing with scrubbers and a CI-safe reporter, and the table of281 what in Feathers's mechanics has been superseded and by what.