Integration Testing on JVM

karimtr1 pts0 comments

Integration Testing on the JVM: My Ideal Process, End to End — The Culture of CodeSkip to main content

Unit tests tell me a function does what I think it does. They don&rsquo;t tell me my service starts, binds its ports, reads its config, talks to a database, consumes from Kafka, and survives an LLM provider returning a 503 mid-stream. That second category is where most production incidents live, and it&rsquo;s the one I care about most.<br>This post lays out the integration-testing process I&rsquo;ve converged on for JVM web services. The examples come from three repositories you can clone and run: koog-spring-boot-assistant (Spring Boot + WebFlux), quarkus-assistant-demo (Quarkus), and Mokksy (for the Docker-image variant). They&rsquo;re Kotlin because suspending functions and a fluent DSL make these tests pleasant to read — but everything here applies to Java too, and with virtual threads on Java 21+ you get the same ergonomics without coroutines.<br>Assume a typical service: a REST API, perhaps a WebSocket or messaging endpoint (Kafka/SQS), a database, and an outbound dependency or two — here, an LLM provider. The system under test (SUT) is a real, booted application, not a sliced @WebMvcTest context.<br>Put end-to-end tests in their own module<br>The decision that pays off most: integration tests live in a separate module , not in src/test alongside your unit tests.<br>In the koog repository the root pom.xml declares two modules:

xml

Copy

2 app<br>3 integration-tests

The integration-tests module depends on app as a black box. It builds the application, then drives it from the outside over HTTP and WebSocket — the same surface a real client sees. No reaching into Spring beans, no @MockBean, no shared application-context tricks.<br>Three reasons the separation earns its keep:<br>The two suites run on different clocks. Unit tests are cheap and run on every save. Integration tests boot a real app and cost real seconds. Mix them, and your fast feedback loop inherits the slow suite&rsquo;s startup cost.<br>Failsafe and Surefire already want this split. Maven&rsquo;s convention runs unit tests in test (Surefire) and integration tests in verify (Failsafe). mvn verify runs everything; mvn test stays fast.<br>The dependency direction stays honest. The test module can only see the public surface, which stops you from accidentally testing implementation details.<br>On Gradle, the same idea maps to a dedicated source set or a separate subproject. The module boundary is the point, not the build tool.<br>Bring the Environment up before the Server<br>There are two layers of infrastructure, and the order matters: the Environment starts first, the Server second.<br>The Environment aggregates everything the SUT depends on: a database, a Kafka or SQS simulator, an HTTP stub for third-party APIs (WireMock or Mokksy), and — for an AI service — an LLM simulator. In the koog repository the LLM side is ai-mocks, Mokksy&rsquo;s OpenAI-shaped mock server:

kotlin

Copy

1object TestEnvironment {<br>2 val mockOpenai = MockOpenai(verbose = true)<br>4 init {<br>5 Awaitility.setDefaultTimeout(5.seconds.toJavaDuration())<br>6 Awaitility.setDefaultPollDelay(500.milliseconds.toJavaDuration())<br>7 Awaitility.setDefaultPollInterval(500.milliseconds.toJavaDuration())<br>9 System.setProperty("OPENAI_API_KEY", "dummyOpenAIKey")<br>10 System.setProperty("spring.profiles.active", "test")<br>11 }<br>12}

Every dependency binds to an ephemeral port. Don&rsquo;t hardcode 5432 or 9092 — let the OS assign a free port and read it back. This is what lets the full suite run on a laptop while Docker is busy with three other projects, and it&rsquo;s a hard requirement for parallel CI.<br>Real downstreams belong in Testcontainers. For dependencies you can&rsquo;t fake faithfully — a real Postgres, a real Redis — the Environment starts them as containers and hands their mapped ports to the Server. Start them individually, or point Testcontainers at a docker-compose.yml so your test topology and your local-dev topology are the same file. One source of truth beats two that drift apart.<br>For Kafka, reach for Redpanda rather than the full Kafka image. It&rsquo;s Kafka-API-compatible, starts in a second or two instead of waiting on a ZooKeeper/KRaft dance, and Testcontainers ships a first-class RedpandaContainer. On a suite where startup time is the budget, that swap alone buys back minutes.<br>The LLM simulator deserves a callout, because it changes what &ldquo;integration test&rdquo; even means for an AI service. A real model is slow, nondeterministic, and costs money per call. Mokksy lets me assert on the request the app sends and script the response — including token-by-token streaming and deliberate failures. A flaky, expensive dependency becomes a fast, deterministic one I fully control.<br>Simulate external services; test the real contract separately<br>That phrase — one I fully control — is the whole argument, and it&rsquo;s worth dwelling on, because the alternative is a trap I&rsquo;ve watched good teams fall into.<br>At one...

rsquo tests integration test real kafka

Related Articles