DEV Community

Kuruba Ramesh
Kuruba Ramesh

Posted on

Integrating Specmatic Contract Testing into Spring PetClinic REST: What a "Simple" DELETE Test Taught Me About Test Architecture

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. schemaResiliencyTests had drifted between local and committed config — my local file correctly had all, but the committed specmatic.yaml on GitHub still said none. That one config file had simply never been committed.
  2. 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)