Setup Java Spring Boot Testcontainers

This documentation was generated with the assistance of AI. Please report any inaccuracies.

Gives a Spring Boot service one definition of its runtime dependencies, used by both mvn verify (or ./gradlew integrationTest) and a developer running the application locally. Works for Java or Kotlin services on Maven or Gradle.

Purpose

The single most valuable property of this setup is that the two paths cannot drift: the integration tests start the same compose.yaml a developer starts, so "works on my machine" and "passes in CI" converge by construction. iru-setup-java-springboot-testcontainers writes that compose file, the Testcontainers ComposeContainer harness that launches it, and the Failsafe/JaCoCo wiring that runs the tests against it.

Two details it treats as non-negotiable, both because they are the usual causes of flaky integration suites:

  • Every image tag is pinned — never latest. An unpinned tag turns a passing suite into a time bomb that fails on an unrelated day for an unrelated reason. Resolved tags are written back into springboot-stack.yml so the compose file, the tests, and the OpenTofu engine versions all agree on one number.

  • Every service has a real healthcheck, not a sleep. Both ComposeContainer and Spring’s own compose support wait for health, so a service without one is considered ready the instant its container starts — and tests then fail intermittently against a database that hasn’t finished initialising.

It also wires Spring Boot’s spring-boot-docker-compose support for the local profile, with skip.in-tests: true so that support and ComposeContainer don’t both try to manage the same stack and fight over ports. Prometheus and Grafana sit behind a compose profiles: key so integration tests don’t pay to start them, while docker compose --profile observability up gives a developer the full local stack with the datasource already provisioned.

The API mock for downstream clients

Whenever the service has REST, gRPC or GraphQL clients, the same compose stack gains an API mocking service, because an integration test that calls a real downstream service is not an integration test of this service: it is slow, it fails when someone else deploys, and it cannot reproduce the error responses the adapter is supposed to handle.

The mock is driven by the contracts already in apis/rest-client/<name>/, apis/grpc-client/<name>/ and apis/graphql-client/<name>/ — the same files each client is generated from. A parallel set of hand-written fixtures is never created, because two descriptions of one contract diverge, and the day they do, the client compiles against one and passes its tests against the other. Unlike Prometheus and Grafana, the mock sits behind no compose profile: tests need it, and so does a developer running the service locally.

Situation Default Why

Any gRPC or GraphQL client, alone or alongside REST clients

Microcks

Mocks REST, gRPC and GraphQL from the same contracts in one container, consuming the OpenAPI spec, the .proto and the SDL directly with no descriptor build step, and can later verify the real downstream service against those same contracts. One tool covering every protocol outweighs any per-protocol advantage.

REST clients only

WireMock

Far lighter and faster to start, with a finer-grained stub DSL for responses, delays, and faults. A wiremock-grpc-extension exists, so the choice is not a dead end if gRPC arrives later — it just adds a protoc --descriptor_set_out build step at that point. GraphQL is the weaker case: every operation is one POST /graphql, so stubs match on request body rather than path and the SDL is never read by the mock at all, leaving contract and mock free to drift.

Both options are always offered, so a team already running Microcks for contract testing elsewhere keeps using it even for a REST-only service. The answer is written back to the manifest as stack.apiMock so a re-run and every later reader agree on one choice. With WireMock, the contract is wired in from both ends: stub mappings are generated from each spec’s examples, and the OpenAPI validation extension makes a request or response that violates the contract fail the test — which is what buys back most of the contract fidelity Microcks gets for free.

The skill announces whether it is creating the stack from scratch or updating an existing one. For each file it owns that already exists (compose.yaml/docker-compose.yml, the *IT base class(es), the Prometheus/Grafana provisioning, the mock’s import/mapping configuration) it warns, names concrete drift (a missing service, a differently pinned image tag) and asks Update (Recommended) / Keep as is / Replace, unless on-existing pre-answers. Replace is never offered for an *IT class containing real test logic beyond the generated base.

It also reads the manifest’s ci: block, which only affects what it reports. With ci.tests: unit (or ci.enabled: false) the generated *IT tests run locally only: the workflows skill runs Maven with -DskipITs, so no Docker/Testcontainers stack starts in CI, and the report says so plainly. The CI runner memory-ceiling warning is likewise only given when ci.tests: full.

Language and build tool

The skill reads the manifest’s project.language (java/kotlin, absent means java) and project.buildTool (maven/gradle, absent means maven). Java follows every step as written. Kotlin writes the test classes under src/test/kotlin from dedicated Kotlin templates (a companion object with @JvmStatic members, Testcontainers 2.x package names such as org.testcontainers.postgresql.PostgreSQLContainer, and ComposeContainer(DockerImageName, File) so no compose CLI is needed on the machine or CI runner, with that image tag pinned and recorded in the manifest). On Gradle it never redefines the integrationTest task: the iru.integration-test convention plugin written by Setup Java Spring Boot Poms already owns it, so the skill only verifies that every module holding an *IT applies that plugin, and adds mock Testcontainers dependencies as version-catalog aliases in gradle/libs.versions.toml rather than with literal versions in module build files. compose.yaml, the observability files and the mock configuration are identical for every language and build tool.

Plugin

Shipped by the iru-springboot plugin of the ai-catalog marketplace (install with /plugin install iru-springboot@ai-catalog; see Plugins).

Invocation

/iru-setup-java-springboot-testcontainers
/iru-setup-java-springboot-testcontainers <args: key: value lines>

Inputs

Argument Required Description Default

stack-file

No

args key: value line giving the manifest path. Read to build the service list: one container per database engine, Redis if cached, Kafka plus a Confluent Schema Registry if messaging is enabled, and Prometheus/Grafana if metrics are enabled. Caffeine needs no container and its absence is reported so it doesn’t look like an omission.

springboot-stack.yml at the repository root

project.language / project.buildTool

No (manifest fields)

java/kotlin and maven/gradle. Select the Java or Kotlin test templates and the Maven (Failsafe/JaCoCo in the pom) or Gradle (integrationTest task from the convention plugin) wiring checks and verification commands.

java and maven

mode

No

args key: value line, new or existing. Informational only: it never means "update silently".

None

on-existing

No

args key: value line, ask, keep, update or replace, pre-answering the per-file Update / Keep as is / Replace question for scripted runs.

ask

ci.tests / ci.enabled

No (manifest fields)

Read from the manifest’s ci: block. Only changes the report: with unit (or CI disabled) the *IT tests are reported as running locally only, and the CI memory-ceiling warning is omitted.

Read from the manifest

stack.apiMock

No

Which API mocking tool backs the service’s REST/gRPC/GraphQL clients. Used without re-asking when the manifest records it; otherwise chosen via AskUserQuestion with the default set by whether any gRPC or GraphQL client exists, and written back to the manifest. Ignored entirely when the service has no clients.

Microcks with gRPC or GraphQL clients, WireMock for REST-only

Client contracts

No

apis/rest-client/<name>/ and apis/grpc-client/<name>/, mounted into the mock container and used to build every mocked response. A REST contract with no examples imports cleanly and then has nothing to serve — the most common mock failure.

Read from the repository

Image tags

No

Taken from the manifest if recorded there; otherwise the current stable release is looked up and written back into the manifest.

Looked up, then persisted

Docker

No

docker info must succeed for the verification step. If Docker isn’t running, every file is still written — they are correct regardless — but verification is skipped and the setup is explicitly reported as unverified.

Verification skipped if unavailable

Outputs

  • compose.yaml at the repository root — one pinned service per technology, each with a real readiness probe, depends_on: condition: service_healthy where ordering genuinely matters, obviously-fake development-only credentials, named volumes only where local state is worth keeping, and bounded resource settings (notably Elasticsearch’s single-node mode and heap cap).

  • observability/prometheus/prometheus.yml and observability/grafana/provisioning/datasources/prometheus.yaml, if metrics are enabled.

  • The mock service, if the service has any client — with the client contracts mounted read-only, plus (for WireMock) generated stub mappings under wiremock/mappings/<name>/, each carrying a header comment naming the contract and operation it came from so the next person edits the spec rather than the mapping, and a descriptor set under wiremock/grpc/ when the gRPC extension is in play.

  • stack.apiMock written back into springboot-stack.yml.

  • The integration-test base class(es) — a shared ComposeContainer-based one for full-context tests, plus narrower per-adapter base classes that start only the one container that module needs, using @ServiceConnection where a single container suffices and @DynamicPropertySource for the compose-based one (Java under src/test/java, or Kotlin under src/test/kotlin). Each client’s clients.<name>.base-url is bound to the mock’s randomly-assigned host port; a client-adapter *IT instead uses the tool’s own Testcontainers module (MicrocksContainer / WireMockContainer), which starts in seconds against the same contract rather than bringing up the whole stack. Its test-scope dependency is added through the root pom’s dependency management on Maven, or a gradle/libs.versions.toml alias on Gradle.

  • On Gradle, a verification (not a redefinition) that iru.integration-test is applied wherever an *IT exists, that ./gradlew test passes with Docker stopped, and that the aggregated coverage report includes the integrationTest execution data.

  • Test-profile configuration per module, with the module’s own migration tooling left enabled so a broken migration fails a test rather than a deploy.

  • A report stating whether the stack was created from scratch or updated, the action taken per pre-existing file (created / updated / replaced / kept as is) with mismatches left in kept files, and where the integration tests run per the manifest’s ci: block (locally only when ci.tests is unit or CI is disabled).

  • A report covering every service and its pinned tag, which containers each base class starts, whether migrations run in tests, and the result of each verification command.

Execution flow

flowchart TD A["Start /iru-setup-java-springboot-testcontainers"] --> B["Step 0: Read the manifest and args;\nbuild the service list; resolve and\npersist image tags; check docker info;\nannounce from scratch vs update and ask\nUpdate / Keep as is / Replace per\nexisting file (or apply on-existing)"] B --> B2{"Any REST or\ngRPC client?"} B2 -- no --> C["Step 1: Write compose.yaml —\npinned images, healthchecks, depends_on,\nfake dev credentials, observability profile"] B2 -- yes --> B3["Resolve stack.apiMock, or ask:\nMicrocks (default with gRPC) vs\nWireMock (default REST-only)"] B3 --> B4["Add the mock service, mounting\napis/rest-client and apis/grpc-client;\ngenerate WireMock mappings from examples"] B4 --> C C --> D["Step 2: Write the integration-test base class(es)\n(ComposeContainer, wait strategies,\nstarted once per JVM, client base URLs\nbound to the mock)"] D --> D2["Kotlin: use Kotlin templates under src/test/kotlin;\nGradle: verify the integrationTest convention plugin\nand use libs.versions.toml aliases"] D2 --> E["Step 3: Wire spring-boot-docker-compose\nfor the local profile (skip.in-tests)"] E --> F["Step 4: Ensure migrations and test\nfixtures run against the containers;\nmock responses live in the contract"] F --> G["Step 5: Verify Failsafe/Surefire split\n(or Gradle integrationTest split)\nand coverage wiring"] G -- mvn test needs Docker --> H["An IT is misnamed as *Test — fix the name"] H --> G G --> I["Step 6: Verify via iru-gate-runner —\ncompose config, mvn test and verify\n(or ./gradlew test and integrationTest),\nlocal stack up/down, and that the mock\nimported its contracts and was really called"] I -->|"compose path, Kafka listeners,\nor Elasticsearch memory"| J["Iterate on the usual suspects"] J --> I I -- Docker unavailable --> K["Report as unverified"] I --> L["Step 7: Report actions per file, where ITs run\n(per ci block), services, base classes,\npersisted tags, and warnings"] K --> L

Dependencies

Invokes

None — it does not call any skill. It reads springboot-stack.yml, writes the compose file and test harness, and updates the manifest’s image tags and stack.apiMock.

It also depends on Setup Java Spring Boot Poms's output rather than invoking it: Failsafe/JaCoCo bindings on Maven, or the iru.integration-test convention plugin and version catalog on Gradle.

Likewise it depends on Setup Java Spring Boot APIs's output rather than invoking it: the client contracts that skill writes under apis/rest-client/ and apis/grpc-client/ are what the mock serves, and the examples it puts in them are what become the mocked responses.

Invoked by

Also runs standalone (/iru-setup-java-springboot-testcontainers) to add a container to an existing service.

  • iru-gate-runner — spawned for each of Step 6’s verification commands, since a full mvn verify (or ./gradlew integrationTest) against a container stack produces a very large log.

Source

SKILL.md on GitHub — the file this page was generated from.