Introduction
Hello, I am Kuruba Ramesh, a Full Stack Developer specialising in the MERN Stack and Java Spring Boot. As part of my ongoing work with the Specmatic Full Stack AI Engineering Internship, I picked a second, well-known project for contract testing integration: spring-petclinic-rest, the official Spring team's REST version of the classic Petclinic sample, built with an API-first approach and a complete OpenAPI 3.0 spec.
Where my first project (a banking API) taught me how Specmatic's resiliency tests generate schema-valid data, this project taught me something more architectural: contract testing can expose limitations in your test infrastructure itself, not just your API.
Repository: https://github.com/KRameshr/spring-petclinic-rest
Project Overview
Technology Stack
- Java 17
- Spring Boot
- Spring Data JPA
- H2 (in-memory database)
- Specmatic 2.50.0 (JUnit 5 integration)
Spring PetClinic REST exposes a full CRUD API for owners, pets, vets, visits, and specialties — 18 paths, 36 operations. It already ships with an OpenAPI spec, external example files, and a specmatic.yaml (Config V3), so unlike a greenfield integration, my job was to get the existing setup fully working, find real gaps, and document them honestly.
# specmatic.yaml
version: 3
specmatic:
settings:
test:
schemaResiliencyTests: all
maxTestRequestCombinations: 1
systemUnderTest:
service:
$ref: "#/components/services/petclinicService"
runOptions:
$ref: "#/components/runOptions/petclinicServiceTest"
components:
services:
petclinicService:
description: Spring Petclinic REST Service
definitions:
- definition:
specs:
- spec:
path: openapi.yml
source:
filesystem:
directory: src/main/resources
data:
examples:
- directories:
- src/main/resources/openapi_examples
runOptions:
petclinicServiceTest:
openapi:
type: test
baseUrl: http://localhost:9966/petclinic/api
schemaResiliencyTests: all is the setting that generates both the positive and negative schema variations, not just the hand-written examples — the same feature that ended up exposing the DELETE issue below.
Setting a Real Baseline
Running the existing contract test suite (SpecmaticContractTest via JUnit 5 and Maven):
221 scenarios generated, 214 passing, 7 failing.
Bug #1: A Missing Field That Broke Every Visit Creation
The first real failure was POST /visits. Every generated request failed with a DataIntegrityViolationException. The request schema (VisitFields) only declared date and description — but the Visit entity has a required foreign key to a pet (petId). Every contract-valid request Specmatic generated was, by definition, missing a field the database required.
Fix: introduced a request-only VisitCreate schema (date + description + petId) in the OpenAPI spec, wired through a new MapStruct mapping:
// VisitMapper.java
@Mapping(target = "id", ignore = true)
@Mapping(source = "petId", target = "pet.id")
Visit toVisit(VisitCreateDto visitCreateDto);
id is deliberately ignored (it's server-generated), and petId on the incoming DTO maps straight onto the JPA relationship (pet.id) MapStruct needs to build the entity. Once petId flowed through the request correctly, this failure disappeared for good — replaced by a far more interesting one.
Bug #2 (or is it?): DELETE Never Succeeds
Six DELETE operations were failing with 404 instead of the expected 200. Here's the actual output from one of them:
Testing scenario "Delete an owner by ID. Response: Owner details found and returned."
API: DELETE /owners/(ownerId:number) -> 200
>> RESPONSE.STATUS
R0002: HTTP status mismatch
Documentation: https://docs.specmatic.io/rules#r0002
Summary: The HTTP status code does not match the expected status code
defined in the specification
Specification expected status 200 but response contained status 404
The obvious diagnosis: their ID path parameters had no minimum/maximum bounds in the spec — unlike their sibling GET/PUT operations for the same resource — so Specmatic generated a fully random int32, which almost never matched a real seeded row.
The fix looked trivial: add the same bounds the GET/PUT operations already had. I tried it.
It made things worse.
With valid bounds, Specmatic now generated an ID that did exist in the database — and the DELETE call actually succeeded, permanently removing a real row (and its dependent pets/visits). That cascaded into 20+ unrelated failures later in the same test run, because other scenarios still expected that data to exist.
So I tried a second approach: a named example tying one specific ID to the DELETE scenario. Same cascade — the delete still went through for real.
Third attempt: dedicated "throwaway" seed rows that no other test depended on, specifically for DELETE to target. By the time the DELETE scenario ran, an earlier POST scenario in the same test run had already filled that "safe" slot with real data.
Three independent experiments, three identical failure modes.
The Actual Root Cause
None of this was a schema bug. ContractTest runs its dynamically generated scenarios against a live, already-running Spring Boot instance over plain HTTP, with no reset between scenarios. Any test that successfully mutates data can affect any other test that runs later in the same suite — and the open-source SpecmaticContractTest JUnit 5 interface has no documented hook to reset application/database state between individual generated scenarios.
I confirmed this wasn't fixable at the schema level. It required test-infrastructure changes (per-scenario database reset) that are out of scope for a contract-testing pass — this is a genuine, separate piece of work.
Verifying the Setup Was Actually Correct
Along the way, a peer review caught two real issues worth mentioning:
-
schemaResiliencyTestshad drifted between local and committed config — my local file correctly hadall, but the committedspecmatic.yamlon GitHub still saidnone. That one config file had simply never been committed. - Six Java files with real exception-handling fixes (returning proper 4xx instead of 500 for malformed requests) had also been sitting uncommitted locally for a while — this was silently causing CI to report 125 failures instead of the expected 7, because CI only sees what's actually pushed.
Both were fixed and pushed, bringing CI back in line with local results.
Key Learnings
1. A "simple" schema fix can hide a cascading side effect.
Adding minimum/maximum bounds looked like the obvious, safe fix for random-ID failures. It was correct in isolation — but the moment a DELETE could succeed for real, it exposed the lack of test isolation. The fix wasn't wrong; the environment it ran in wasn't ready for it.
2. Repeat the experiment before you trust the result.
The DELETE-cascade problem wasn't obvious after one attempt — it took three independent approaches (schema bounds, named examples, dedicated seed rows) hitting the identical failure mode before I was confident it was architectural, not incidental.
3. Local ≠ committed ≠ CI.
The schemaResiliencyTests drift is a reminder that "it works on my machine" isn't the same as "it's actually in the repository." Regularly diffing local state against git status and CI output catches this kind of silent gap.
4. Contract testing can find bugs in the test suite, not just the API.
This was the most valuable lesson from this project. Specmatic didn't just validate the OpenAPI contract — running it honestly, without forcing a green build, surfaced a structural limitation in how the tests themselves are built.
Current Status
221 tests, 214 passing, 7 known and root-cause-documented failures — CI now consistently matches local results (it still reports failure because of those 7 documented cases, which is expected, not a regression). Full write-up, including the three-experiment DELETE investigation, in the repository README.
Conclusion
Where my first Specmatic project taught me that schema constraints are testing instructions, this one taught me that a contract test suite is only as reliable as the infrastructure it runs on. Fixing the app's contract violations was straightforward; discovering — and clearly documenting, rather than papering over — a genuine gap in test isolation was the harder and more useful part of the work.
Repository: https://github.com/KRameshr/spring-petclinic-rest
Top comments (0)