skip to content

How do you run a Spring Boot app at dev time against Testcontainers-managed services?

level: middleimportance: should knowfreq 38%

answer

  1. Launch the app from the test classpath
  2. A test-scoped main method, not a test
  3. SpringApplication.from(...).with(...)
  4. bootTestRun and spring-boot:test-run
  5. Same @TestConfiguration serves tests and dev

basics

~10 s

Add a test-scoped main class that calls SpringApplication.from(MyApplication::main).with(TestcontainersConfiguration.class).run(args). Launch it from the IDE, or with the Gradle bootTestRun task or the Maven spring-boot:test-run goal, and the containers start with the app.

solid answer

~40 s

Spring Boot 3.1 added `SpringApplication.from(...)`, which lets a class in `src/test/java` launch the real application and augment it with extra configuration. You write a small `TestMyApplication` whose `main` calls `SpringApplication.from(MyApplication::main).with(TestcontainersConfiguration.class).run(args)`, reusing the same configuration class your integration tests import. Running that class starts Postgres, Kafka or whatever else the configuration declares, wires the app to them, and stops them when you stop the app. Build tooling exposes it directly: the Spring Boot Gradle plugin adds a `bootTestRun` task and the Maven plugin a `spring-boot:test-run` goal, both of which run the application with the test classpath. The payoff is that a new developer needs only Docker — no locally installed database, no shared dev environment, and no drift between what tests run against and what you develop against.

code

java · 9 lines
java
// src/test/java/com/example/TestMyApplication.java
public class TestMyApplication {

    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main)
                .with(TestcontainersConfiguration.class)
                .run(args);
    }
}

go deeper

for a junior

Know that a Spring Boot project can start its own Postgres or Kafka container when you run it locally, that Docker must be running, and that the entry point is a small main class in the test sources.

for a middle

Be able to write the launcher from memory — SpringApplication.from(MyApplication::main).with(SomeTestcontainersConfiguration.class).run(args) — and name the bootTestRun task and spring-boot:test-run goal that do the same thing from the build.

for a senior

Talk about the operational payoff: one container definition shared by CI and local development, pinned image tags, and how restart behaviour and image pulls shape the inner loop for the team.

for a principal

Weigh this against a compose-managed stack or a shared dev environment: who owns the lifecycle, how onboarding time and environment drift change, and what it costs to require Docker on every developer machine and build agent.

## The problem it solves Integration tests had a good story for infrastructure — start a container, point the context at it — but local development did not. Developers installed Postgres by hand, ran an ad-hoc `docker compose` stack, or shared a remote dev database. Each of those drifts from what CI actually tests, and each is a step in the onboarding document that goes stale. Spring Boot 3.1 closed the gap by making the *test* classpath a legitimate way to launch the application. ## The mechanism `SpringApplication.from(...)` takes a method reference to your production `main` method — typically `MyApplication::main`. It does not run it immediately; it returns an object you can augment. `.with(Class<?>... configurations)` adds extra configuration classes on top of whatever the production main would have built, and `.run(String... args)` finally launches. So the launcher is a few lines in `src/test/java`, conventionally named after the application class with a `Test` prefix. Because it lives in the test source set, it can see the same `@TestConfiguration` class your `@SpringBootTest` classes import, and it never ships in the production jar. The container beans behave exactly as they do in a test: they implement Testcontainers' `Startable`, so Boot's `spring-boot-testcontainers` integration starts them as the context comes up and stops them when the context closes. Connection details reach the app through the same mechanism the tests use — you are not duplicating wiring, you are reusing it. ## Running it Three entry points, all equivalent: **From the IDE** — run the test-scoped main class like any other main method. This is the common path, and it gives you the debugger and hot-restart behaviour you already use. **Gradle** — the Spring Boot plugin contributes a `bootTestRun` task, which is `bootRun`'s sibling running against the test runtime classpath. **Maven** — the Spring Boot plugin contributes a `spring-boot:test-run` goal with the same intent. All three require Docker to be available, because there is nothing fake about the dependencies: a real Postgres process is running in a real container. ## Interaction with devtools and restarts With `spring-boot-devtools` on the classpath, a code change triggers a restart of the application context. Restarting a context normally means stopping its beans — including the containers — and paying full startup cost again. Boot's Testcontainers integration is aware of the restart classloader and keeps containers alive across a devtools restart rather than cycling them, which is what makes the workflow usable. If your loop still feels slow, the sibling concern is container reuse, which keeps containers alive even across JVM exits. ## What it does not replace This is a development convenience, not a deployment story. The containers are throwaway: data written during a session is gone when the process stops unless you configure otherwise. If you need seeded data, do it the same way tests do — an init script, a migration tool run at startup, or a `CommandLineRunner` guarded by a profile. It also does not replace `docker compose` for the case where you want the stack running independently of the app process. Boot has a separate Docker Compose integration for that; the Testcontainers route is the right one when you want the app to own the lifecycle of its dependencies and the same definition to serve both tests and dev. ## Why interviewers ask It separates people who have adopted the annotation from people who understand the wiring. The instructive part of the answer is that the dev-time launcher and the integration test share *one* configuration class: if a candidate describes maintaining two definitions — one for tests and one for local running — that is the drift the feature exists to remove. ## Practical cautions Keep the launcher class out of the pattern your test task picks up (a class named `TestMyApplication` is not matched by the usual `*Tests`/`*Test` conventions, which is why the prefix form is the recommended name). Pin image tags so local runs and CI agree. And remember that the first run on a machine pulls images, so the initial startup is much slower than steady state.

  • Why put the launcher in src/test/java rather than in the main sources?
    Because the container definitions and the Testcontainers dependency are test-scoped: keeping the launcher beside them means nothing container-related ships in the production jar, and it can reuse the very same `@TestConfiguration` the integration tests import. A main-source launcher would drag Testcontainers into production runtime.
  • What happens to the containers when devtools restarts the context?
    Boot's Testcontainers integration is aware of the devtools restart and keeps the containers running instead of stopping and recreating them, so a code change costs a context restart rather than a fresh database start. Without that, the workflow would be too slow to use.
  • How would you get seed data into the dev-time database?
    The same ways a test does: an init script or SQL mounted into the container image, your normal migration tool running at application startup, or a profile-guarded `CommandLineRunner` that inserts fixtures. Nothing is persisted between runs unless you deliberately arrange it.

saying these in an interview costs you the question

  • Maintains separate container definitions for tests and local dev
  • Thinks bootRun starts the containers too
  • Puts Testcontainers on the production runtime classpath
  • Believes data survives after the dev-time app stops
  • Names the launcher so the test task tries to run it as a test

context