skills/testing/java-test-updater/SKILL.md
Updates broken JUnit tests after a deliberate code change — distinguishing tests that broke because the behavior changed (update assertion) from tests that broke because they were overcoupled to structure (loosen or delete). Use after API changes, refactors, or intentional behavior changes leave a trail of failing tests.
npx skillsauth add santosomar/general-secure-coding-agent-skills java-test-updaterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
A refactor broke 47 tests. Some of them should break — the behavior changed. Some of them broke because they were mocking internals. Don't bulk-update assertions; triage first.
| Failure cause | Signal | Action |
| ------------------------------------- | --------------------------------------------------- | ------------------------ |
| Behavior changed (intentional) | Assertion on return value fails | Update assertion |
| API signature changed | Compilation error — method renamed, param added | Update call site |
| Over-mocked internals | verify(mock).someInternalMethod(...) fails | Delete or loosen mock |
| Over-specific assertion | assertEquals("User[id=1, name=Joe]", obj.toString()) | Loosen — assert fields, not string |
| Test was testing a bug | Old assertion was wrong; new behavior is correct | Update + document fix |
| Regression (unintentional) | Assertion fails, but the change wasn't supposed to affect this | Stop. This is a real bug. |
The last row is why you don't auto-update. A failing test might be right.
mvn test 2>&1 | tee test-output.log
Compilation errors (cannot find symbol, method X cannot be applied) are API drift — mechanical fixes. Assertion failures need judgment.
| Change | Fix |
| ----------------------------------- | --------------------------------------------------------- |
| Method renamed | IDE rename-propagation, or sed. Grep for old name. |
| Param added | Find default/sentinel. Or: was the old method kept as an overload? Use it. |
| Return type changed | Update assignment type. If narrowed, cast may be wrong — check. |
| Class moved/renamed | Update imports. Bulk: IDE organize-imports. |
| Checked exception added | Add throws to test method, or wrap in assertDoesNotThrow if the test is about the happy path |
For each failing assertion, read the change that caused it:
// Test:
assertEquals(new BigDecimal("27.80"), invoice.total());
// Fails: actual is 27.55
// Git blame the change:
// commit abc123 "Fix: tax rounds half-even, not half-up"
Half-even rounding is a bug fix. The old 27.80 was wrong. Update:
// Fixed in abc123: half-even rounding. Was 27.80 (half-up bug).
assertEquals(new BigDecimal("27.55"), invoice.total());
Versus:
// Test:
verify(taxCalculator).calc(eq(subtotal), eq(Region.US));
// Fails: calc was called with (subtotal, "US") — String, not enum
// Change: TaxCalculator.calc now takes String region, not Region enum
The test is verifying an internal call signature. It broke because an internal API changed. This test is over-coupled. Fix the immediate break, but also: does this verify need to exist? Is the call to taxCalculator the behavior, or an implementation detail?
| Pattern | Why fragile | Loosen to |
| ---------------------------------------------- | ------------------------------------------ | ------------------------------------------ |
| verify(mock, times(3)).helper() | Call count is implementation | verify(mock, atLeastOnce()).helper() or delete |
| assertEquals("exact string", obj.toString()) | toString is debug output | Assert fields: assertEquals("Joe", obj.name()) |
| assertThat(list).containsExactly(a, b, c) | Order might not be contractual | .containsExactlyInAnyOrder(a, b, c) if order isn't spec'd |
| Mocking classes in the same package | Tests structure, not behavior | Use real objects; mock only boundaries |
| PowerMock on static/private | Deepest coupling possible | Refactor for testability, delete test |
@Test void price_bulk_discount() {
// 100 units → 10% bulk discount
assertEquals(new BigDecimal("900.00"), engine.price(bulkOrder(100)));
}
// Now fails: actual 1000.00
The change was to TaxCalculator. Why did bulk discount break? This is a regression. Don't update the assertion. The test is doing its job — it caught a bug introduced by an unrelated change.
git bisect to find where, → regression-root-cause-analyzer to find why.
Once you've triaged all 47 and confirmed none are regressions:
// For snapshot/approval tests: one command updates all
// ApprovalTests: delete .received files after review, rename to .approved
// For explicit assertions: no shortcut — update each with its reason
There is no safe bulk-update for explicit assertions. Each one asserts something on purpose.
verify. Some call verifications are the behavior — "sends an email" is a behavior you verify by checking the email client was called.@Disabled as a fix. Disabled tests are deleted tests that still clutter the file.## Failing tests
Total: <N> Compile: <N> Assertion: <N>
## Compile fixes (mechanical)
| Test | Error | Fix | Applied |
| ---- | ----- | --- | ------- |
## Assertion triage
| Test | Old expected | New actual | Cause | Classification | Action |
| ---- | ------------ | ---------- | ----- | -------------- | ------ |
(Classification: intentional-change | bug-fix | over-coupled | REGRESSION)
## Regressions found
<tests that are correctly failing — bugs in the change, not the test>
## Over-coupling cleanup
<verify/toString/PowerMock patterns loosened>
## After
Passing: <N> Updated: <N> Loosened: <N> Deleted: <N> Regression bugs filed: <N>
development
Extracts human-readable pseudocode from a verified formal artifact (Dafny, Lean, TLA+) while preserving the verified properties as annotations, so the proof-carrying logic can be reimplemented in a production language. Use when porting verified code to an unverified target, when documenting what a formal spec actually does, or when handing a verified algorithm to an implementer.
development
Translates natural-language or pseudocode descriptions of concurrent and distributed systems into TLA+ specifications ready for the TLC model checker. Identifies state variables, actions, type invariants, safety properties, and liveness properties from the description. Use when formalizing a protocol, when the user describes a distributed algorithm to verify, when designing a consensus or locking scheme, or when starting formal verification of a concurrent system.
testing
Reduces a TLA+ model so TLC can actually check it — shrinks constants, adds state constraints, abstracts data, or applies symmetry — when the state space is too large to enumerate. Use when TLC runs out of memory, when checking takes hours, or when a spec works at N=2 and you need confidence at larger scale.
development
TLA+-specific instance of model-guided repair — reads a TLC error trace, identifies the enabling condition that should have been false, strengthens the corresponding action, and maps the fix to source code. Use when TLC reports an invariant violation or deadlock and you have the code-to-TLA+ mapping from extraction.