API Mocking and Testing With Microcks

Contract-first APIs are only as good as the mocks backing them, and Microcks is the tool I keep reaching for when I need realistic test doubles without building a fake service from scratch. I wrote up how I use it to mock and test APIs across microservices, catching integration issues before they ever hit a shared env...

Most of us reach for WireMock or MockServer first. Nothing wrong with that, honestly, they're solid tools and I still use WireMock for quick one-off stubs. But there's a gap: those tools mock what you tell them to mock. They don't know or care whether your stub actually matches the real contract. You write the stub once, the real API drifts, and your tests keep smiling at you while production throws 500s. I ran into exactly this on a job-ingestion integration we built to pull external listings into the platform (we wrap a few RapidAPI-style feeds for sourcing). The vendor's JSON schema had an optional field that became required in a minor version bump. Our WireMock stub didn't care. Our integration tests didn't care. Our staging environment didn't care either, because staging pointed at the same stale mock. The first place we noticed was production, when the ingestion job started silently dropping records. Not great. Microcks flips that around because the mock is generated straight from the contract. Change the OpenAPI spec, regenerate the mock, and any drift between what you're testing and what you're actually building against becomes visible immediately, instead of three months later when someone finally checks the vendor's changelog. The Contract Itself: What Microcks Actually Reads Before touching Testcontainers or Gradle config, it helps to see the actual artifact Microcks parses, because everything downstream depends on it being reasonably complete. Here's a trimmed version of the OpenAPI file I use for the external job feed integration: openapi: 3.0.3 info: title: External Job Feed API version: "1.0.0" paths: /jobs: get: operationId: listJobs parameters: - name: location in: query required: false schema: type: string responses: "200": description: A page of job listings content: application/json: schema: $ref: "#/components/schemas/JobFeedResponse" components: schemas: JobFeedResponse: type: object required: - jobs properties: jobs: type: array items: $ref: "#/components/schemas/JobListing" JobListing: type: object required: - title - postedAt properties: title: type: string company: type: string location: type: string postedAt: type: string format: date-time remote: type: boolean That required list under JobListing is the piece I got burned by earlier. Marking postedAt as required in the schema but never populating it in my examples file is exactly the kind of mismatch that a conformance test catches and a hand-rolled WireMock stub never would, because WireMock doesn't know the schema exists. The secondary artifact, the examples file, is where I keep realistic sample data separate from the spec itself: external-job-feed-examples: listJobs: 200: jobs: - title: "Senior Java Backend Engineer" company: "Acme Remote Systems" location: "San Jose, Costa Rica" postedAt: "2026-01-14T09:30:00Z" remote: true - title: "React Frontend Developer" company: "LatAm Cloud Partners" location: "Remote - LATAM" postedAt: "2026-01-13T15:05:00Z" remote: true Keeping these separate from the OpenAPI file matters more than it looks. If I crammed examples into the spec itself, the spec would balloon every time I wanted a slightly different mock scenario. Splitting them means I can maintain three or four example sets (empty results, single result, malformed vendor payload for negative testing) without touching the contract that actually defines the shape. Getting Microcks Running: More Config Than People Expect Before I even get to the JUnit test, there's some setup worth writing down, because the first time I tried this I underestimated how many moving pieces there are. For local manual poking around, outside of tests, I keep a small docker-compose.yml that spins up a standalone Microcks instance and mounts my contracts as a volume: version: "3.8" services: microcks: image: quay.io/microcks/microcks-uber:1.9.1 ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=default - MICROCKS_URL=http://localhost:8080 volumes: - ./contracts:/deployments/config/artifacts That gives me a UI at localhost:8080 where I can browse services, inspect generated mocks, and manually trigger conformance test runs against a real deployment while I'm still designing the spec. It's genuinely useful before you wire anything into CI. For the Gradle side, the test dependencies I actually use in the jobs-posting backend look roughly like this: dependencies { testImplementation "io.github.microcks:microcks-testcontainers:0.4.0" testImplementation "org.testcontainers:junit-jupiter:1.20.4" testImplementation "org.springframework.boot:spring-boot-starter-webflux" } Check the Microcks docs for the exact artifact coordinates before you pin a version. They've shuffled group IDs a couple of times as the project matured, and I've been burned once by copying a stale dependency line from an old blog post. Then, because I want the same test suite to run either against a Testcontainers instance locally and in CI, or against a long-lived Microcks deployment in a shared environment, I externalize the target through properties instead of hardcoding it: microcks: url: ${MICROCKS_URL:http://localhost:8080} api-token: ${MICROCKS_API_TOKEN:} timeout-ms: ${MICROCKS_TEST_TIMEOUT_MS:8000} And in the CI pipeline itself, the environment variables that get injected look like this: MICROCKS_URL=https://microcks.internal.itjobopportunities.io MICROCKS_API_TOKEN=${{ secrets.MICROCKS_API_TOKEN }} MICROCKS_TEST_TIMEOUT_MS=8000 That split matters more than it looks. If your test suite only knows how to talk to a container that Testcontainers spun up, you can't reuse the same tests to smoke-check a staging deployment later. Parameterizing the endpoint up front saved me from writing two versions of the same test. A Fuller Test, Not Just a Smoke Check The version of this I originally showed people was a single happy-path assertion, and honestly that undersells what Microcks actually catches. Here's closer to what we run for real against the jobs-posting backend's external job feed integration, with both a positive mock test and a schema conformance check in the same class: @Testcontainers class JobFeedContractTest { private static final String OPENAPI_SPEC = "external-job-feed-openapi.yaml"; private static final String EXAMPLES = "external-job-feed-examples.yaml"; @Container static MicrocksContainer microcks = new MicrocksContainer( DockerImageName.parse("quay.io/microcks/microcks-uber:1.9.1")) .withMainArtifacts(OPENAPI_SPEC) .withSecondaryArtifacts(EXAMPLES); static RestClient restClient; @BeforeAll static void setUp() { String mockUrl = microcks.getRestMockEndpoint("External Job Feed API", "1.0.0"); restClient = RestClient.create(mockUrl); } @Test void mockRespectsContractShape() { JobFeedResponse response = restClient.get() .uri("/jobs?location=costa-rica") .retrieve() .body(JobFeedResponse.class); assertThat(response.jobs()).isNotEmpty(); assertThat(response.jobs().get(0).title()).isNotBlank(); assertThat(response.jobs().get(0).postedAt()).isNotNull(); } @Test void mockedResponsesConformToOpenApiSchema() { MicrocksClient client = new MicrocksClient(microcks.getHttpEndpoint()); TestRequest request = TestRequest.builder() .serviceId("External Job Feed API:1.0.0")...