An agent wrote 53 candidate test classes across three Petclinic runs. The checks kept 40 and refused 13.
I removed the code the write tests claimed to cover and ran them again.
Removing each of five saves made the matching HTTP tests fail. Petclinic’s own mocked controller tests still passed. The full mutation experiment has the code and results on Boot 2.7.3 and 4.1.2.
A passing assertion can say very little
The weak examples below passed early checks during adversarial testing, separate from the 53 real candidates and 13 refusals.
| Weak test | What the assertion misses |
|---|---|
A read checks 2xx and contains("<html") or contains(" ") | Those values can appear on every page |
A read checks Veterinarians from shared navigation | The menu doesn’t show that the requested page’s own content rendered |
A test claims PUT /api/users/{id} but writes to /api/users/me | It exercises a sibling route |
| A read-back matches a value the test seeded | The asserted value doesn’t depend on the write it claims to check |
A test counts seeded James Carter as evidence for a page that never renders it | The data exists in setup without being evidence in that page’s response |
Follow the value into the assertion.
For a write, find the value that changes. For a read, find the text that belongs to the requested page. If the assertion would still pass when that part of the app is broken, it needs more work.
Check the endpoint that actually receives the request
The adversarial tests included /api/users/me;x=1. Spring and Tomcat strip that matrix parameter before routing, so the request still goes to the sibling route.
The test must send its write through the endpoint it claims to cover.
Compare the HTTP method and resolved route with the handler. A test named for PUT /api/users/{id} can still be testing /api/users/me.
A write test needs a value that depends on the write
The kept Petclinic tests start the real app on a random port and use TestRestTemplate with the real database.
For owner creation, the test sends a fresh UUID as the address in the form POST to /owners/new. It then requests /owners?lastName=Httpcreatedowner through the running app and asserts that the response contains the marker.
The read-back is a different GET. The assertion depends on the value sent by this write, which makes the missing save visible.
For an update, asserting a seeded value is too weak. The read-back has to check the changed value. Otherwise the original row can satisfy the assertion even when the update never takes effect.
The owner edit mutation in Petclinic produced two failures. One test checked the updated address and another checked the updated city. Both failed when the handler’s save was removed.
For DELETE, seed the record and see it through the app. Delete it through the endpoint, then check that it’s gone.
Review the setup and final assertion together. If the setup alone can explain the response being accepted, the write hasn’t been demonstrated by that assertion.
A read test needs content the page renders
The real candidate tests were mostly good. They asserted seeded data known to be rendered by the target page.
| Requested page | Rendered value asserted |
|---|---|
/owners/1/edit | George Franklin |
/vets.html | James Carter |
/vets.html?page=2 | Sharon Jenkins |
/vets | radiology |
The weak real candidate checked only "<form" and "lastName" on /owners/find. It was refused. Generic markup and a field name didn’t meet the checks for that page.
Shared layout text has the same problem. Veterinarians in a navigation menu can be present even when the requested page’s own content hasn’t been demonstrated.
Read the template to check where the asserted string comes from.
Remove the save and run the suite
The direct check for the write tests was to break the write. Each project copy had one this.owners.save(owner); line removed, then ran:
./mvnw test
The unmodified baseline passed. All five broken endpoint copies failed. Only the matching HTTP tests failed in each copy.
That result held on the original Boot 2.7.3 application and on the final 4.1.2 code. Each suite had 52 tests with 1 skipped. The failures by endpoint were 1 for owner creation, 2 for owner editing, 1 for pet creation, 1 for pet editing, and 1 for adding a visit.
Check the cause too. A test that fails while loading its application context hasn’t demonstrated a broken save. In the Boot 4 migration, a duplicate cache stopped tests before any request was sent. That was a configuration problem requiring its own fix.
Run the project’s build before keeping the tests
One kept test failed Petclinic’s spring-javaformat validate goal. Formatting checks had been skipped while the tests were being written.
The fix was to run the project’s formatter on the generated files, then run the project’s own checks. A test that passes under a reduced writing-time command can still fail the build used by the project.
The checks kept 40 classes and refused 13. Run the normal project build before keeping any generated file.
The AI generated tests guide is related background. For this Petclinic work, review the request route and follow the submitted value into the final assertion. Then check what happened when the save disappeared.
Related Articles
Java Modernization Readiness Assessment
15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.