<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kuruba Ramesh</title>
    <description>The latest articles on DEV Community by Kuruba Ramesh (@krameshr).</description>
    <link>https://dev.to/krameshr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3981848%2F7a8f30d7-1044-4f8c-9275-09e699536538.png</url>
      <title>DEV Community: Kuruba Ramesh</title>
      <link>https://dev.to/krameshr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krameshr"/>
    <language>en</language>
    <item>
      <title>Integrating Specmatic Contract Testing into Spring PetClinic REST: What a "Simple" DELETE Test Taught Me About Test Architecture</title>
      <dc:creator>Kuruba Ramesh</dc:creator>
      <pubDate>Mon, 13 Jul 2026 05:24:42 +0000</pubDate>
      <link>https://dev.to/krameshr/-integrating-specmatic-contract-testing-into-spring-petclinic-rest-what-a-simple-delete-test-5he1</link>
      <guid>https://dev.to/krameshr/-integrating-specmatic-contract-testing-into-spring-petclinic-rest-what-a-simple-delete-test-5he1</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello, I am Kuruba Ramesh. As part of my ongoing work with the Specmatic Full Stack AI Engineering Internship, I integrated contract testing into 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.&lt;/p&gt;

&lt;p&gt;This post covers four things: the architecture of the setup, exactly what I did step by step, the real gaps I found in Specmatic itself along the way (not just bugs in the application), and how I ultimately designed the CI pipeline around the one gap that couldn't be closed outright.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/KRameshr/spring-petclinic-rest" rel="noopener noreferrer"&gt;https://github.com/KRameshr/spring-petclinic-rest&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The application: A Spring Boot REST API (Java 17, Spring Data JPA, H2 in-memory database) managing a veterinary clinic — owners, pets, vets, visits, and specialties. 18 API paths, 36 operations, full CRUD.&lt;/p&gt;

&lt;p&gt;The contract: The project already ships with an OpenAPI 3.0 spec (&lt;code&gt;src/main/resources/openapi.yml&lt;/code&gt;) — a machine-readable document describing every endpoint, every request/response shape, and every possible status code. For example, here's what it declares for fetching a single owner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;/owners/{ownerId}&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getOwner&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ownerId&lt;/span&gt;
        &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
        &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
          &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
          &lt;span class="na"&gt;maximum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
    &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;200&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Owner details found and returned.&lt;/span&gt;
      &lt;span class="na"&gt;404&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Owner not found.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single block is enough for Specmatic to generate multiple test scenarios: a valid request expecting 200, an out-of-range ID expecting 404, and so on — all without anyone writing a line of test code.&lt;/p&gt;

&lt;p&gt;How Specmatic fits in: Specmatic reads the OpenAPI spec and generates test scenarios from it automatically — no test code written by hand. It sends real HTTP requests to the application (already running, on &lt;code&gt;localhost:9966&lt;/code&gt;) and checks whether every response matches what the spec promises. This is contract testing: verifying the app honours its own documented contract, not just that it "works."&lt;/p&gt;

&lt;p&gt;The configuration (&lt;code&gt;specmatic.yaml&lt;/code&gt;, Config V3) ties it all together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;specmatic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;schemaResiliencyTests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
      &lt;span class="na"&gt;maxTestRequestCombinations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;systemUnderTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;$ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#/components/services/petclinicService"&lt;/span&gt;
&lt;span class="na"&gt;components&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;petclinicService&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openapi.yml&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;src/main/resources/openapi_examples&lt;/span&gt;
  &lt;span class="na"&gt;runOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;petclinicServiceTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9966/petclinic/api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This points Specmatic at the spec file, the base URL of the running app, a folder of external examples for controlling test data, and enables schema resiliency testing — generating negative/edge-case variations, not just the happy path.&lt;/p&gt;

&lt;p&gt;The wiring: a JUnit 5 test class (&lt;code&gt;ContractTest.java&lt;/code&gt;) implements Specmatic's &lt;code&gt;SpecmaticContractTest&lt;/code&gt; interface. Running &lt;code&gt;mvn test -Dtest=ContractTest&lt;/code&gt; reads this configuration, loads the spec, generates scenarios, and runs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by Step: What I Did
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Verified the existing setup actually worked.&lt;/strong&gt;&lt;br&gt;
The project already had &lt;code&gt;specmatic.yaml&lt;/code&gt; (Config V3), a JUnit 5 test class, and external examples. First step was simply running it and getting a real, reproducible baseline: 221 scenarios, some passing, some not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Investigated every failure — not just made the build green.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;POST /visits&lt;/code&gt; failed for every generated request. Tracing it: the request schema was missing a required field (&lt;code&gt;petId&lt;/code&gt;) that the database enforced. This was a real spec-vs-database mismatch, not a testing artifact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Fixed the missing field.&lt;/strong&gt;&lt;br&gt;
Added a request-only schema (&lt;code&gt;VisitCreate&lt;/code&gt;), a MapStruct mapping method, and a controller update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// VisitMapper.java&lt;/span&gt;
&lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ignore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Mapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"petId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pet.id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Visit&lt;/span&gt; &lt;span class="nf"&gt;toVisit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VisitCreateDto&lt;/span&gt; &lt;span class="n"&gt;visitCreateDto&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-ran the suite — this failure was gone for good.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Confirmed schema resiliency testing was active.&lt;/strong&gt;&lt;br&gt;
This is what surfaced most of what follows — negative/edge-case variations catch mismatches the happy path alone never would.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Added a dictionary for realistic test data.&lt;/strong&gt;&lt;br&gt;
Instead of random garbage strings for names and addresses, I wrote &lt;code&gt;openapi_dictionary.yaml&lt;/code&gt; with real-looking values (names, cities, phone numbers).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Verified the dictionary — and found it wasn't actually being applied.&lt;/strong&gt;&lt;br&gt;
Specmatic's log confirmed it loaded the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Using dictionary file /usr/src/app/src/main/resources/openapi_dictionary.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But checking an actual generated request showed the values were still random:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v-Sc-YI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"M."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"XNPBY"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dictionary keys (&lt;code&gt;Owner&lt;/code&gt;, &lt;code&gt;Vet&lt;/code&gt;) didn't match the real schema names (&lt;code&gt;OwnerFields&lt;/code&gt;, &lt;code&gt;VetFields&lt;/code&gt;) — a silent mismatch with no warning. Fixing the key names fixed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before&lt;/span&gt;
&lt;span class="na"&gt;Owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;James&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Mary&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Robert&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Patricia&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# after&lt;/span&gt;
&lt;span class="na"&gt;OwnerFields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;James&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Mary&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Robert&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Patricia&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Re-checked a generated request — real values now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Patricia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Johnson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Madison"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This unexpectedly exposed a second bug: &lt;code&gt;POST /visits&lt;/code&gt; was returning &lt;code&gt;201&lt;/code&gt; while the spec said &lt;code&gt;200&lt;/code&gt;. Fixed that too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Found and fixed a caching feature the spec promised but the code never built.&lt;/strong&gt;&lt;br&gt;
The spec declared an ETag header for conditional GET requests. The app never sent one. Added one file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ETagConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="nf"&gt;shallowEtagHeaderFilter&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ShallowEtagHeaderFilter&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verified real &lt;code&gt;304 Not Modified&lt;/code&gt; responses now work, with zero controller changes needed across any of the 18 endpoints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Traced the remaining 6 DELETE failures to their root cause — five separate attempts.&lt;/strong&gt;&lt;br&gt;
Schema ID bounds, a named example, dedicated seed rows, coverage-focused 404 examples, and a dictionary override for the ID. Every attempt hit the same wall (detailed in the Gaps section below): &lt;code&gt;SpecmaticContractTest&lt;/code&gt; runs all generated scenarios as a single dynamic JUnit 5 stream with no per-scenario reset hook, so once any scenario mutates real seed data — a DELETE that actually succeeds, for instance — every later scenario touching that row is exposed to the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API: DELETE /owners/(ownerId:number) -&amp;gt; 200
  &amp;gt;&amp;gt; RESPONSE.STATUS
      R0002: HTTP status mismatch
      Specification expected status 200 but response contained status 404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;9. Closed the gap by designing around the constraint instead of fighting it.&lt;/strong&gt;&lt;br&gt;
Once the root cause was confirmed five separate ways, I stopped looking for a trick that would avoid it and instead rebuilt around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Made the seed data fully dense within its valid ID bounds — no gaps. A gap anywhere in a bounded range is exactly what schema-driven boundary and negative-mutation tests probe first, so it's a guaranteed eventual collision, not just a risk.&lt;/li&gt;
&lt;li&gt;Split the resiliency-testing configuration into its own CI job. &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; generates randomized boundary and negative-mutation tests across every operation, including DELETE, and is fundamentally incompatible with fixed, dedicated test records when there's no fixture isolation between scenarios — there's always some chance a random DELETE mutation targets the same row as a named example before that example gets its turn. So the pipeline now runs two jobs: &lt;code&gt;contract-tests&lt;/code&gt; (named examples only, deterministic, required) and &lt;code&gt;resilience-tests&lt;/code&gt; (full resiliency, &lt;code&gt;continue-on-error: true&lt;/code&gt;, reports honestly without blocking the pipeline).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: &lt;code&gt;contract-tests&lt;/code&gt; now passes 100%, reliably, confirmed across many repeated runs both locally and in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;10. Found a genuine Hibernate/JPA bug by refusing to accept "flaky" as an explanation.&lt;/strong&gt;&lt;br&gt;
While bringing the rest of the test suite (all unit and integration tests across all four &lt;code&gt;ClinicService&lt;/code&gt; variants — H2/JDBC, HSQL/JDBC, JPA, Spring Data JPA) to a fully passing state, two of those variants had one test failing intermittently: deleting a &lt;code&gt;PetType&lt;/code&gt; sometimes threw&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TransientPropertyValueException: Persistent instance of 'Pet' references
an unsaved transient instance of 'PetType'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracing it instead of writing it off as test-order flakiness found this in the actual repository code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PetType&lt;/span&gt; &lt;span class="n"&gt;petType&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;petType&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;petType&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;petType&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// ... then, moments later:&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;em&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createQuery&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DELETE FROM PetType WHERE id="&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;petTypeId&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;executeUpdate&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An entity-level &lt;code&gt;em.remove()&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; a bulk JPQL &lt;code&gt;DELETE&lt;/code&gt; on the same row, in the same method. Bulk JPQL statements force an intermediate flush of the persistence context, and if any other &lt;code&gt;Pet&lt;/code&gt; still held an in-memory reference to that &lt;code&gt;PetType&lt;/code&gt;, Hibernate correctly refused to proceed rather than silently corrupting state. The fix: remove the redundant &lt;code&gt;em.remove()&lt;/code&gt; call — the bulk delete already does the job. The exact same bug existed in both the JPA and Spring Data JPA repository implementations, likely copy-pasted at some point in the project's history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;11. Documented everything, including what didn't work and why.&lt;/strong&gt;&lt;br&gt;
The README covers every fix, the failed attempts, and the final CI design — with actual log excerpts, not just summaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specmatic Gaps I Found
&lt;/h2&gt;

&lt;p&gt;This is the part I think is most useful to share — not application bugs, but real limitations in Specmatic itself (open-source edition) that I ran into.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 1: No hook to reset state between dynamically generated scenarios.&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;SpecmaticContractTest&lt;/code&gt; / &lt;code&gt;SpecmaticJUnitSupport&lt;/code&gt; generates all scenarios as a single dynamic JUnit 5 test stream. Standard JUnit lifecycle hooks like &lt;code&gt;@BeforeEach&lt;/code&gt; only apply to individual &lt;code&gt;@Test&lt;/code&gt; methods and cannot attach to this stream. &lt;code&gt;configureTest()&lt;/code&gt; runs once, for the entire suite — not per scenario. I confirmed this five separate ways — schema bounds, a named example, dedicated seed rows, coverage-focused 404 examples, and a dictionary override — all reproducing the identical cascade once a real ID could be targeted. Full fixture isolation between scenarios is an Enterprise-only feature; in the open-source tier, the correct response is to design your data and CI gates so the constraint can't cause a false failure — dense, gap-free ID ranges plus a separate, non-blocking job for resiliency testing — rather than keep searching for a way around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 2: Dictionary key mismatches fail silently.&lt;/strong&gt;&lt;br&gt;
If a dictionary's top-level key doesn't exactly match the OpenAPI schema name, Specmatic doesn't apply those values — and doesn't warn you either. It simply falls back to random generation, while the log still reports the dictionary file as "loaded." This cost real debugging time, since "loaded successfully" reads like confirmation that it's working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 3: Dictionary values don't apply to path parameters.&lt;/strong&gt;&lt;br&gt;
I tried forcing a specific ID via the dictionary to make a DELETE scenario target a known-safe row. Confirmed this doesn't work — dictionary overrides only apply to schema body fields, not path parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 4: Automatic dictionary generation is Enterprise-only.&lt;/strong&gt;&lt;br&gt;
Specmatic can generate a dictionary automatically from a spec and existing examples — but only in the Enterprise edition. The open-source edition requires writing dictionaries by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 5: The &lt;code&gt;--filter&lt;/code&gt; CLI option isn't available through the JUnit5/Maven integration.&lt;/strong&gt;&lt;br&gt;
Specmatic's CLI supports excluding specific operations (e.g. &lt;code&gt;--filter="!(METHOD='DELETE')"&lt;/code&gt;) from a run, which would have been a clean way to skip DELETE from resiliency testing specifically. That option isn't exposed through &lt;code&gt;SpecmaticJUnitSupport&lt;/code&gt;. I tried passing it as a system property (&lt;code&gt;-DFILTER=...&lt;/code&gt;); it had no effect on the JUnit run, and in one test actually correlated with &lt;em&gt;more&lt;/em&gt; failures rather than acting as a no-op — worth knowing before assuming it will transfer over from CLI usage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gap 6: Writing a reliable 304/ETag example is hard when the underlying value is dynamic.&lt;/strong&gt;&lt;br&gt;
Even after fixing the app to genuinely support ETags, writing a Specmatic example for the 304 case is awkward — the ETag is a hash of the response body, so it changes whenever seed data is mutated elsewhere in the same test run. A hardcoded example expires the moment other tests touch the same data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"The log says it loaded" is not the same as "it's being used."&lt;/strong&gt; Always verify against an actual generated request, not just the tool's own status message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixing one bug can unmask another.&lt;/strong&gt; The &lt;code&gt;POST /visits&lt;/code&gt; status mismatch was invisible until the dictionary fix let requests actually reach the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeat an experiment before trusting the conclusion.&lt;/strong&gt; Five independent attempts at the same DELETE problem, all failing identically, is what turned a guess into a documented, confident root cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Intermittent" is a description of the symptom, not a diagnosis.&lt;/strong&gt; Both the dictionary key mismatch and the later Hibernate flush-order bug looked like tooling flakiness right up until they were traced to a specific, deterministic line of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Some limitations are real and should be designed around, not endlessly worked around.&lt;/strong&gt; Once a constraint is confirmed — not assumed — the better use of time is building around it honestly (a &lt;code&gt;continue-on-error&lt;/code&gt; resiliency job alongside a fully deterministic required gate) rather than continuing to search for a trick that avoids it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A bug duplicated across two files is a strong signal of copy-paste history.&lt;/strong&gt; Finding the identical flush-order mistake in both &lt;code&gt;JpaPetTypeRepositoryImpl&lt;/code&gt; and &lt;code&gt;SpringDataPetTypeRepositoryImpl&lt;/code&gt; was a good reminder to check sibling implementations whenever a bug turns up in one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Current Status
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Specmatic &lt;code&gt;ContractTest&lt;/code&gt;: 254 scenarios, &lt;strong&gt;100% deterministic pass&lt;/strong&gt; on the required &lt;code&gt;contract-tests&lt;/code&gt; CI gate; a separate &lt;code&gt;resilience-tests&lt;/code&gt; job runs full schema resiliency testing and reports honestly, non-blocking.&lt;/li&gt;
&lt;li&gt;Full &lt;code&gt;mvn verify&lt;/code&gt;: &lt;strong&gt;491/491 tests passing&lt;/strong&gt; — every REST controller test, all four &lt;code&gt;ClinicService&lt;/code&gt; variants, plus config and validator tests — with JaCoCo coverage checks passing.&lt;/li&gt;
&lt;li&gt;API contract coverage: &lt;strong&gt;60%&lt;/strong&gt;, up from 42% at the start of this work.&lt;/li&gt;
&lt;li&gt;All three GitHub Actions workflows (Specmatic Contract Tests, Java CI, Docker Hub build) are green.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full technical detail — including exact log excerpts for every experiment above, the complete list of bugs fixed, and the CI design decisions — is in the repository README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The most valuable part of this project wasn't fixing the obvious bugs — it was the discipline of verifying tool behaviour instead of trusting it, confirming a root cause multiple times before accepting it, and then, once a real limitation was confirmed, designing around it honestly instead of continuing to chase a workaround. That discipline is what turned six unexplained DELETE failures into a fully deterministic, 100%-passing required test gate, and what turned an "intermittent" test into a genuine, fixed Hibernate bug duplicated across two files.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/KRameshr/spring-petclinic-rest" rel="noopener noreferrer"&gt;https://github.com/KRameshr/spring-petclinic-rest&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>springboot</category>
      <category>testing</category>
    </item>
    <item>
      <title>Contract Testing a Spring Boot Banking API with Specmatic: A Complete Implementation Guide</title>
      <dc:creator>Kuruba Ramesh</dc:creator>
      <pubDate>Fri, 12 Jun 2026 19:37:56 +0000</pubDate>
      <link>https://dev.to/krameshr/how-i-integrated-specmatic-contract-testing-into-a-real-banking-api-valuemeters-cak</link>
      <guid>https://dev.to/krameshr/how-i-integrated-specmatic-contract-testing-into-a-real-banking-api-valuemeters-cak</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I'm &lt;strong&gt;Kuruba Ramesh&lt;/strong&gt;, a Full Stack Developer specializing in the MERN stack and Java Spring Boot. As part of the &lt;strong&gt;Specmatic Full Stack AI Engineering Internship Assessment&lt;/strong&gt;, I completed a hands-on project focused on &lt;strong&gt;spec-first engineering&lt;/strong&gt; and contract-driven API development.&lt;/p&gt;

&lt;p&gt;The assessment asked me to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Complete the Specademy course on Spec-First Engineering&lt;/li&gt;
&lt;li&gt;Integrate &lt;a href="https://specmatic.io" rel="noopener noreferrer"&gt;Specmatic&lt;/a&gt; into a real-world Spring Boot application&lt;/li&gt;
&lt;li&gt;Configure automated contract testing and CI&lt;/li&gt;
&lt;li&gt;Document the implementation, the challenges, and what I learned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For this, I integrated Specmatic into my banking API project, &lt;strong&gt;ValueMeters&lt;/strong&gt;, and automated provider contract testing with GitHub Actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://krameshdev.vercel.app/" rel="noopener noreferrer"&gt;krameshdev.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article walks through the full implementation — what contract testing actually is, how I wired Specmatic into a JWT-secured Spring Boot app, the schema resiliency failures that taught me the most, and how the CI pipeline is structured today.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Contract Testing, and Why Does It Matter?
&lt;/h2&gt;

&lt;p&gt;Before taking the Specademy course, I knew unit testing and integration testing well, but &lt;strong&gt;contract testing&lt;/strong&gt; was new to me. One idea from the course stuck with me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Contract testing is compiler safety for API calls.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In a monolithic application, the compiler catches a type mismatch the moment you try to build. You cannot ship code that calls a function with the wrong arguments — the build simply fails.&lt;/p&gt;

&lt;p&gt;In a distributed system, services talk to each other over HTTP instead of function calls. If one service changes a field name, a data type, or a status code, there's no compiler to catch it. The mismatch only surfaces at runtime — often in production, and often at the worst possible time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://specmatic.io" rel="noopener noreferrer"&gt;Specmatic&lt;/a&gt; closes that gap. It reads your &lt;strong&gt;OpenAPI specification&lt;/strong&gt; and turns it into an executable contract. Using the &lt;a href="https://docs.specmatic.io" rel="noopener noreferrer"&gt;Specmatic documentation&lt;/a&gt; as a reference, I set it up to automatically validate, on every test run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request and response schemas&lt;/li&gt;
&lt;li&gt;HTTP status codes&lt;/li&gt;
&lt;li&gt;Response headers and content types&lt;/li&gt;
&lt;li&gt;Whether every documented endpoint is actually implemented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the running application ever drifts from what the OpenAPI spec promises, the test fails immediately — the same way a compiler error stops a bad build before it ships.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Overview: ValueMeters Banking API
&lt;/h2&gt;

&lt;p&gt;For this assessment, I used my banking API project, &lt;strong&gt;ValueMeters&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technology Stack
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Spring Boot 2.7.18&lt;/li&gt;
&lt;li&gt;Spring Security with JWT authentication&lt;/li&gt;
&lt;li&gt;Spring Data JPA&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;Specmatic 2.48.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 18&lt;/li&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;API Documentation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SpringDoc OpenAPI 3.0&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  API Surface
&lt;/h3&gt;

&lt;p&gt;The application exposes 13 REST endpoints across five feature areas:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;/auth/register&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Register a new user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/auth/login&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Log in and receive a JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/account/user/{userId}&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Get an account by user ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/account/{accountNumber}&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Get an account by account number&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/transaction/deposit/{accountId}&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Deposit money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/transaction/withdraw/{accountId}&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Withdraw money&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/transaction/transfer/{fromAccountId}&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Transfer money between accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/transaction/history/{accountId}&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Get transaction history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/expense/add/{accountId}&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Add an expense&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/expense/list/{accountId}&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;List all expenses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/expense/summary/{accountId}&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Get an expense summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/budget/set/{accountId}&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Set budget limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/budget/get/{accountId}&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;Get budget limits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since the project already used SpringDoc OpenAPI, generating a spec was straightforward, which made ValueMeters a good candidate for Specmatic integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Specmatic
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Adding the Dependency
&lt;/h3&gt;

&lt;p&gt;I added the Specmatic JUnit 5 support dependency (version 2.48.0) along with Spring Boot Actuator, which Specmatic uses to auto-discover every registered endpoint and enforce coverage governance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing the OpenAPI Specification
&lt;/h3&gt;

&lt;p&gt;I wrote an OpenAPI spec describing every endpoint's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request schema&lt;/li&gt;
&lt;li&gt;Response schema&lt;/li&gt;
&lt;li&gt;Path parameters&lt;/li&gt;
&lt;li&gt;Status codes (200, 400, 404)&lt;/li&gt;
&lt;li&gt;Error response shapes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This spec became the single source of truth that both the application and Specmatic are validated against.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring &lt;code&gt;specmatic.yaml&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="na"&gt;systemUnderTest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;definitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;definition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
          &lt;span class="na"&gt;specs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openapi.json&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openapi&lt;/span&gt;
    &lt;span class="na"&gt;runOptions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;openapi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
        &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9000"&lt;/span&gt;
        &lt;span class="na"&gt;actuatorUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:9000/actuator/mappings"&lt;/span&gt;
        &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PATH!='/api-docs,/swagger-ui'"&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;examples&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;directories&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;examples&lt;/span&gt;

&lt;span class="na"&gt;specmatic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;schemaResiliencyTests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;governance&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;successCriteria&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maxMissedOperationsInSpec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="na"&gt;minCoveragePercentage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
      &lt;span class="na"&gt;enforce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three settings do most of the work here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;actuatorUrl&lt;/code&gt;&lt;/strong&gt; — Specmatic queries Spring Actuator to discover every registered endpoint, so nothing gets silently skipped from coverage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;examples&lt;/code&gt; directory&lt;/strong&gt; — externalized, deterministic test data instead of relying purely on generated values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;schemaResiliencyTests: all&lt;/code&gt;&lt;/strong&gt; together with &lt;strong&gt;&lt;code&gt;governance&lt;/code&gt;&lt;/strong&gt; — this is what actually generates the negative and boundary-mutation scenarios, and then enforces that 100% of operations are covered with zero missed operations. Resiliency isn't a separate testing phase bolted on afterward — it's the same Specmatic run, driven by this one setting.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Biggest Challenge: JWT Authentication
&lt;/h2&gt;

&lt;p&gt;ValueMeters uses JWT authentication — every endpoint expects a valid token in the &lt;code&gt;Authorization&lt;/code&gt; header. When Specmatic fired its test requests, Spring Security rejected them before they ever reached a controller, well before Specmatic could evaluate the contract at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution: A Separate Test Security Profile
&lt;/h3&gt;

&lt;p&gt;Rather than weakening production security, I used Spring profiles to swap in a test-only configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"!test"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecurityConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Production JWT configuration — untouched&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestSecurityConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecurityFilterChain&lt;/span&gt; &lt;span class="nf"&gt;testFilterChain&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpSecurity&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csrf&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;csrf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authorizeHttpRequests&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;anyRequest&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;permitAll&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps production security completely untouched while giving Specmatic full access to exercise the API during the &lt;code&gt;test&lt;/code&gt; profile only.&lt;/p&gt;




&lt;h2&gt;
  
  
  Writing the Contract Test
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@SpringBootTest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webEnvironment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpringBootTest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;WebEnvironment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEFINED_PORT&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@ActiveProfiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@Sql&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scripts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/data.sql"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;executionPhase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ExecutionPhase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BEFORE_TEST_METHOD&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BankingContractTest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;SpecmaticJUnitSupport&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details here cost me real debugging time, and are worth calling out explicitly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;DEFINED_PORT&lt;/code&gt;, not &lt;code&gt;RANDOM_PORT&lt;/code&gt;.&lt;/strong&gt; With &lt;code&gt;RANDOM_PORT&lt;/code&gt;, Spring Boot starts on a random port, but Specmatic connects to the port declared in the OpenAPI spec. Since those never match, every single test fails with "Connection refused" — and the error gives no hint that the port is the actual problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reseed the database before every test method with &lt;code&gt;@Sql&lt;/code&gt;.&lt;/strong&gt; Provider tests need predictable state. Without a consistent reseed, resiliency's randomized requests can land on data that was mutated by a previous test, producing flaky, order-dependent failures.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Valuable Debugging Session: Schema Resiliency Failures
&lt;/h2&gt;

&lt;p&gt;The most useful thing I learned from this whole assessment came from debugging why Specmatic's &lt;strong&gt;schema resiliency&lt;/strong&gt; run was failing tests that had nothing wrong with them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;My first positive test passed. But the resiliency run then generated a login request with a random, schema-valid email like &lt;code&gt;ahgtg@vismx.com&lt;/code&gt; instead of the seeded &lt;code&gt;test@example.com&lt;/code&gt;. The API correctly rejected it with &lt;code&gt;400 Invalid credentials&lt;/code&gt; — but Specmatic's example expected &lt;code&gt;200&lt;/code&gt;, so it flagged the mismatch as a failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root Cause
&lt;/h3&gt;

&lt;p&gt;Schema resiliency doesn't just replay your examples — it actively mutates fields based on the constraints declared in the schema. My &lt;code&gt;email&lt;/code&gt; field had &lt;code&gt;"format": "email"&lt;/code&gt; in the OpenAPI schema, so Specmatic generated random-but-valid email addresses on each resiliency pass. My seeded H2 test database only had one real user, so anything else correctly failed login — which is exactly what a real client hitting the real API would experience, but not what the fixed example expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"example"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"example"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Removing &lt;code&gt;"format": "email"&lt;/code&gt; stopped Specmatic from generating random email-shaped values during resiliency runs, so it fell back to the example value instead — which matched the seeded test data.&lt;/p&gt;

&lt;p&gt;Importantly, this did &lt;strong&gt;not&lt;/strong&gt; weaken validation anywhere that matters: Spring's &lt;code&gt;@Email&lt;/code&gt; and &lt;code&gt;@Valid&lt;/code&gt; annotations still enforce email format at runtime, in production. The schema change only affects how Specmatic &lt;em&gt;generates test data&lt;/em&gt;, not how the application &lt;em&gt;validates real requests&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Register Endpoint Needed the Same Treatment
&lt;/h3&gt;

&lt;p&gt;The register endpoint also needed its own externalized example file (&lt;code&gt;examples/auth_register_success.json&lt;/code&gt;). Without one, Specmatic logged a warning that it was ignoring the inline spec example and falling back to fully random schema-based data — which hit the same problem as the email field.&lt;/p&gt;




&lt;h2&gt;
  
  
  Expanding to Full API Coverage
&lt;/h2&gt;

&lt;p&gt;With the JWT and resiliency issues resolved, I expanded the OpenAPI spec to cover all 13 endpoints — account, transaction, expense, and budget — not just auth.&lt;/p&gt;

&lt;p&gt;For each endpoint, I:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tightened request schemas, removing loosely-typed optional fields that caused unpredictable resiliency mutations&lt;/li&gt;
&lt;li&gt;Documented the real response codes (200, 400, 404) instead of only the happy path&lt;/li&gt;
&lt;li&gt;Added externalized positive and negative example files under &lt;code&gt;examples/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fixed service-layer exceptions to map to the correct HTTP status — &lt;code&gt;AccountNotFoundException&lt;/code&gt; → 404, &lt;code&gt;InsufficientBalanceException&lt;/code&gt; → 400&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Final result: 96/96 tests passing, 100% API coverage across all 13 endpoints.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  GitHub Actions CI Integration
&lt;/h2&gt;

&lt;p&gt;Once the tests were solid locally, I automated them with GitHub Actions so every push gets contract-tested automatically, with no manual step required.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the Pipeline Is Structured
&lt;/h3&gt;

&lt;p&gt;Contract tests and schema resiliency are &lt;strong&gt;not two separate test suites&lt;/strong&gt; — resiliency is just &lt;code&gt;schemaResiliencyTests: all&lt;/code&gt; inside the same Specmatic run described above. So the CI pipeline runs everything as a &lt;strong&gt;single job&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Specmatic CI&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contract-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Contract Tests (with schema resiliency)&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up JDK &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;17"&lt;/span&gt;
          &lt;span class="na"&gt;distribution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temurin"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Make mvnw executable&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chmod +x mvnw&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Contract Tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./mvnw test -Dtest=BankingContractTest -Dspring.profiles.active=test&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Contract Report&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;contract-report&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build/reports/specmatic/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every run of this job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boots the app in the &lt;code&gt;test&lt;/code&gt; profile with &lt;code&gt;TestSecurityConfig&lt;/code&gt; active&lt;/li&gt;
&lt;li&gt;Runs the full contract test class, which exercises named examples &lt;em&gt;and&lt;/em&gt; the schema resiliency mutations together&lt;/li&gt;
&lt;li&gt;Enforces governance — zero missed operations, 100% coverage&lt;/li&gt;
&lt;li&gt;Generates a Specmatic HTML report and &lt;strong&gt;uploads it as a GitHub Actions artifact&lt;/strong&gt;, attached directly to that workflow run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deliberately upload the report as a &lt;strong&gt;workflow artifact&lt;/strong&gt; rather than publishing it to GitHub Pages. An artifact is tied to the exact commit and run that produced it, so the report you're looking at always matches the code you're looking at — there's no separate, manually-published copy that can quietly go stale while the code moves on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issues I Fixed to Get CI Green
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;mvnw&lt;/code&gt; not executable on Linux.&lt;/strong&gt; Windows creates &lt;code&gt;mvnw&lt;/code&gt; without the executable bit set. GitHub Actions runs on Ubuntu, so CI failed with exit code 126 until I added &lt;code&gt;chmod +x mvnw&lt;/code&gt; as an explicit step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong Specmatic version.&lt;/strong&gt; Version &lt;code&gt;0.28.0&lt;/code&gt; isn't published on Maven Central — CI failed with a dependency resolution error. I checked &lt;a href="https://central.sonatype.com/artifact/io.specmatic/junit5-support" rel="noopener noreferrer"&gt;Maven Central&lt;/a&gt; directly and pinned to &lt;code&gt;2.48.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;RANDOM_PORT&lt;/code&gt; vs &lt;code&gt;DEFINED_PORT&lt;/code&gt;.&lt;/strong&gt; Same issue as locally, but CI made it impossible to ignore — Specmatic couldn't connect at all until I switched to &lt;code&gt;DEFINED_PORT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two CI jobs doing the same work.&lt;/strong&gt; Early on, I had &lt;code&gt;contract-tests&lt;/code&gt; and &lt;code&gt;resiliency-tests&lt;/code&gt; as two separate GitHub Actions jobs, both running the exact same command. Since resiliency is just a setting inside one Specmatic run, not a distinct test type, this duplicated the same execution and produced two nearly-identical reports for no benefit. Consolidating to the single job above cut CI time roughly in half and matches what's actually true about how Specmatic runs.&lt;/p&gt;

&lt;p&gt;CI is green on every push: &lt;a href="https://github.com/KRameshr/valuemeters-specmatic/actions" rel="noopener noreferrer"&gt;github.com/KRameshr/valuemeters-specmatic/actions&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Schema constraints drive resiliency test data, not just documentation.&lt;/strong&gt; &lt;code&gt;format: email&lt;/code&gt; tells Specmatic to &lt;em&gt;generate&lt;/em&gt; random valid-looking emails during resiliency. Removing the format constraint pins generation back to your example. Schema constraints and runtime validation serve different purposes — don't confuse the two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Every endpoint needs its own positive example.&lt;/strong&gt; Without an externalized example, Specmatic falls back to fully random schema-based data, which may not match your seeded test state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Exception types drive contract accuracy.&lt;/strong&gt; &lt;code&gt;RuntimeException&lt;/code&gt; → 500, &lt;code&gt;AccountNotFoundException&lt;/code&gt; → 404, &lt;code&gt;IllegalArgumentException&lt;/code&gt; → 400. Getting these right at the service layer is what makes the &lt;em&gt;contract&lt;/em&gt; accurate, not just the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Governance turns coverage into a gate, not a metric.&lt;/strong&gt; Enforcing &lt;code&gt;minCoveragePercentage: 100&lt;/code&gt; and &lt;code&gt;maxMissedOperationsInSpec: 0&lt;/code&gt; makes it structurally impossible to quietly ship an untested endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Provider testing needs deterministic state.&lt;/strong&gt; Reseeding with &lt;code&gt;@Sql&lt;/code&gt; before every test method removed an entire category of flaky, order-dependent failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Resiliency testing and contract testing are the same thing, not two pipelines.&lt;/strong&gt; This was the biggest process lesson from this assessment. Schema resiliency is a &lt;em&gt;setting&lt;/em&gt; on your contract test run, not a separate suite that needs its own job, its own report, or its own place in your CI. Treating it as separate — as I initially did with two CI jobs — just duplicates work and reporting for no additional signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Consumer-driven contract testing&lt;/li&gt;
&lt;li&gt;Deeper Specmatic provider examples for edge cases&lt;/li&gt;
&lt;li&gt;Using Specmatic as a mock server for the React frontend during local development&lt;/li&gt;
&lt;li&gt;Expanding negative-scenario coverage further&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Before this project, I thought of OpenAPI mainly as documentation — something Swagger UI renders nicely, not something that actively enforces behavior. Working through this assessment changed that. An OpenAPI spec, run through &lt;a href="https://specmatic.io" rel="noopener noreferrer"&gt;Specmatic&lt;/a&gt;, becomes an executable contract that continuously checks the real application against what was promised, on every single push.&lt;/p&gt;

&lt;p&gt;I integrated Specmatic 2.48.0 into a JWT-secured Spring Boot application, reached 96/96 tests passing with 100% API coverage across 13 endpoints, wired schema resiliency and governance into a single CI job, and learned — the hard way, through a failing test — that every schema constraint is an instruction to Specmatic's test generator, not just a documentation detail.&lt;/p&gt;

&lt;p&gt;That last point is the one I keep coming back to: &lt;strong&gt;contract testing is compiler safety for API calls&lt;/strong&gt;, and Specmatic is what makes that safety net actually executable instead of aspirational.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Specmatic:&lt;/strong&gt; &lt;a href="https://specmatic.io" rel="noopener noreferrer"&gt;specmatic.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specmatic Documentation:&lt;/strong&gt; &lt;a href="https://docs.specmatic.io" rel="noopener noreferrer"&gt;docs.specmatic.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://krameshdev.vercel.app/" rel="noopener noreferrer"&gt;krameshdev.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/KRameshr/valuemeters-specmatic" rel="noopener noreferrer"&gt;github.com/KRameshr/valuemeters-specmatic&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions (live CI):&lt;/strong&gt; &lt;a href="https://github.com/KRameshr/valuemeters-specmatic/actions" rel="noopener noreferrer"&gt;github.com/KRameshr/valuemeters-specmatic/actions&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>specmatic</category>
      <category>java</category>
      <category>springboot</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
