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.
Install with Tessl CLI
npx tessl i github:santosomar/general-secure-coding-agent-skills --skill java-test-updater97
Quality
96%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
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.logCompilation 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 enumThe 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.00The 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 reasonThere 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>47d56bb
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.