Skip to content

Integration Testing Interview Questions & Answers

15 questions Updated 2026-06-26 Share:

Full-context Spring Boot integration testing — @SpringBootTest web environments, TestRestTemplate and WebTestClient, Testcontainers with @ServiceConnection, context caching, @Transactional rollback vs real commits, @ActiveProfiles, and test data setup.

Read the in-depth guideSpring Boot Integration Testing with @SpringBootTest and Testcontainers(opens in new tab)
15 of 15

An integration test loads the full application context with @SpringBootTest and exercises multiple layers wired together — controller → service → repository → database — the way they run in production. A slice loads only one layer; a unit test loads none.

@SpringBootTest                       // entire context, real beans
class OrderFlowIT {
    @Autowired OrderService service;  // the real service, not a mock
    @Autowired OrderRepository repo;  // the real repository + DB
}

Because it boots everything (and often a real database), it's the slowest and most realistic tier — the top of the test pyramid. You keep these few and high-value, covering critical end-to-end flows rather than every branch.

Rule of thumb: Use integration tests to prove the layers work together; keep them few and lean on faster unit/slice tests for breadth.

It decides how the web layer runs during the test:

@SpringBootTest(webEnvironment = WebEnvironment.MOCK)        // default
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) // real server, random port
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)// real server, configured port
@SpringBootTest(webEnvironment = WebEnvironment.NONE)        // no web env at all
  • MOCK (default): a mock servlet environment, no real port — pair with @AutoConfigureMockMvc to use MockMvc.
  • RANDOM_PORT: starts a real embedded server on a free port (injected via @LocalServerPort) — for true HTTP tests.
  • NONE: no web environment, for testing non-web beans in a full context.

Rule of thumb: Use RANDOM_PORT + a real HTTP client when you need genuine network behavior; MOCK + MockMvc otherwise.

With RANDOM_PORT Spring auto-configures a TestRestTemplate (or WebTestClient) pointed at the running server, so you make actual HTTP requests over a socket and assert on the response.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class OrderApiIT {
    @Autowired TestRestTemplate rest;

    @Test void createsOrder() {
        var resp = rest.postForEntity("/orders", new OrderRequest("widget"), Order.class);
        assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(resp.getBody().getStatus()).isEqualTo("NEW");
    }
}

Unlike MockMvc, this goes through the real servlet container — connection handling, filters, serialization, the lot. TestRestTemplate is fault-tolerant (it won't throw on 4xx/5xx, so you can assert error responses). WebTestClient is the fluent, reactive-friendly alternative.

Rule of thumb: Use TestRestTemplate/WebTestClient with RANDOM_PORT for true end-to-end HTTP; use MockMvc when you don't need a real server.

Testcontainers is a library that spins up real services in Docker containers — Postgres, Redis, Kafka, RabbitMQ — for the duration of your tests, then tears them down. You test against the same engine you run in production instead of an in-memory substitute.

@Testcontainers
@SpringBootTest
class OrderIT {
    @Container
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
    // ... point Spring's datasource at db (see @ServiceConnection)
}

The win is fidelity: H2 quietly diverges from Postgres on JSON columns, sequences, upserts, and native SQL. A static container is shared across the test class (and reused across classes if started once), keeping the cost manageable.

Rule of thumb: Use Testcontainers when your tests depend on database/broker-specific behavior — it trades a little startup time for "tests run on the real thing."

@ServiceConnection (Boot 3.1+) auto-configures Spring's connection properties from the container — no manual @DynamicPropertySource to copy the JDBC URL, username, and password.

@Testcontainers
@SpringBootTest
class OrderIT {
    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
    // Spring's DataSource is wired to this container automatically
}

Before 3.1 you needed the verbose form:

@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
    r.add("spring.datasource.url", db::getJdbcUrl);
    r.add("spring.datasource.username", db::getUsername);
    r.add("spring.datasource.password", db::getPassword);
}

Boot recognizes the container type and maps it to the right properties.

Rule of thumb: On Boot 3.1+, prefer @ServiceConnection over @DynamicPropertySource — less boilerplate and fewer property typos.

Booting the context is expensive, so the Spring TestContext framework caches it and reuses it across test classes that request the same configuration. The cache key is the combination of config classes, active profiles, properties, @MockBeans, etc. — change any and you get a new context (a fresh, slow boot).

// These two share ONE cached context (same config) — fast:
@SpringBootTest class ATest { }
@SpringBootTest class BTest { }

// A different profile = a separate context = another full boot:
@SpringBootTest @ActiveProfiles("special") class CTest { }

Things that fragment the cache: varying @MockBean sets, @TestPropertySource values, @DirtiesContext, and different profiles. A suite that accidentally creates dozens of distinct contexts can take minutes longer than necessary.

Rule of thumb: Keep test configuration uniform so contexts are reused; treat each unique config combination as another full application startup.

@DirtiesContext tells Spring the test modified the context in a way that shouldn't leak, so it closes and rebuilds the context (evicting it from the cache). Powerful, but it forces an expensive restart, so use it sparingly.

@SpringBootTest
class CacheResetIT {
    @Test
    @DirtiesContext   // rebuild the context after this test
    void mutatesSingletonState() { /* ... */ }
}

It's a code smell more often than not — usually the real fix is to reset the mutated state in an @AfterEach (clear a cache, reset a mock) rather than nuking the whole context. Each @DirtiesContext pays the full startup cost again.

Rule of thumb: Reach for @DirtiesContext only when state genuinely can't be reset otherwise — prefer cleaning up in teardown to preserve context caching.

When a test method (or class) is @Transactional, Spring wraps it in a transaction and rolls back at the end by default, so each test leaves the database clean without manual cleanup.

@SpringBootTest
@Transactional   // each test rolls back automatically
class OrderServiceIT {
    @Autowired OrderService service;
    @Test void createsOrder() { service.place("widget"); /* rolled back after */ }
}

The catch: the rollback can hide bugs and mislead. Lazy associations may resolve because the session stays open for the whole test (masking LazyInitializationException you'd hit in prod), and a real HTTP test (RANDOM_PORT) runs the server on a different thread/transaction, so the test's transaction doesn't wrap it — rollback won't apply.

Rule of thumb: @Transactional rollback keeps DB tests isolated, but don't rely on it for RANDOM_PORT HTTP tests and beware it can hide lazy-loading and commit-time issues.

Activate a profile with @ActiveProfiles and put test-only settings in application-test.yml. This lets the test run with its own datasource, faster password hashing, disabled schedulers, etc.

@SpringBootTest
@ActiveProfiles("test")
class OrderIT { /* uses application-test.yml */ }
# src/test/resources/application-test.yml
spring:
  jpa:
    hibernate:
      ddl-auto: create-drop
logging:
  level:
    org.hibernate.SQL: DEBUG

For one-off overrides without a whole profile, use @TestPropertySource(properties = "...") or the properties = {...} attribute on @SpringBootTest. Remember each distinct profile/property set is a separate cached context.

Rule of thumb: Use @ActiveProfiles("test") + application-test.yml for shared test config; @TestPropertySource for small per-test overrides.

A few standard approaches, roughly in order of preference:

  • @Transactional rollback — cheapest; the DB resets automatically after each test.
  • @Sql scripts — run SQL before/after a test for precise, repeatable data.
  • Repository/TestEntityManager setup in @BeforeEach — programmatic seeding.
@SpringBootTest
@Sql("/seed-orders.sql")                                   // before each test
@Sql(scripts = "/clean.sql", executionPhase = AFTER_TEST_METHOD)
class OrderIT { /* ... */ }

The cardinal rule is isolation: a test must not depend on data left by another, or order-dependent failures appear. With RANDOM_PORT (no rollback), explicitly clean up — e.g. repo.deleteAll() in @AfterEach or a Testcontainers fresh database.

Rule of thumb: Make each test set up its own data and clean up after itself; never rely on the leftovers of a previous test.

Use @WebMvcTest when you want to test the controller in isolation with mocked services (fast, focused). Use @SpringBootTest when you want the request to flow through the real service and repository down to the database — a true end-to-end check.

@WebMvcTest      → does the controller map/validate/serialize correctly? (services mocked)
@SpringBootTest  → does POST /orders actually persist an order end to end? (real beans)

The trade-off is speed vs. realism: most controller behavior is well covered by the fast slice; reserve the full-context test for a handful of critical flows where the integration itself is the risk.

Rule of thumb: Slice-test most controllers; promote to @SpringBootTest only for the few flows where the value is in the layers working together.

Don't Thread.sleep. Poll for the expected outcome with Awaitility, which retries an assertion until it passes or a timeout fires — robust against timing variance.

service.processAsync(orderId);   // @Async / scheduled / messaging

await().atMost(Duration.ofSeconds(5))
       .untilAsserted(() ->
           assertThat(repo.findById(orderId).get().getStatus()).isEqualTo("DONE"));

A fixed sleep is either too short (flaky) or too long (slow). Awaitility checks frequently and stops the instant the condition holds. For @Async you can also inject the future/CompletableFuture and get() it where the API exposes one.

Rule of thumb: Test eventual/async outcomes by polling with Awaitility, never by sleeping a guessed-at duration.

Because the port is chosen at runtime, inject it with @LocalServerPort (or let an autowired TestRestTemplate/WebTestClient resolve the base URL for you).

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class OrderApiIT {
    @LocalServerPort int port;
    @Autowired TestRestTemplate rest;   // already knows the base URL

    @Test void health() {
        // Either use the injected client (relative path):
        assertThat(rest.getForObject("/actuator/health", String.class)).contains("UP");
        // Or build the URL yourself from the port:
        var url = "http://localhost:" + port + "/actuator/health";
    }
}

RANDOM_PORT avoids "address already in use" clashes when tests run in parallel or on CI. The injected TestRestTemplate is the simplest path — it's pre-pointed at the running server.

Rule of thumb: Use @LocalServerPort (or the auto-configured client) — never hard-code a port with RANDOM_PORT.

The usual offenders:

  • Overusing @SpringBootTest for things a slice or unit test covers — slow suites.
  • Fragmenting the context cache with many @MockBean/profile/property combinations.
  • Order-dependent tests that share mutable data and fail when reordered or parallelized.
  • Thread.sleep for async instead of Awaitility — flaky.
  • Relying on @Transactional rollback in a RANDOM_PORT test (the server runs on another thread).
  • Testing against H2 when prod is Postgres — use Testcontainers for fidelity.
  • Scattering @DirtiesContext and paying repeated full restarts.
// Flaky + DB-fidelity smell rolled into one:
service.processAsync(id);
Thread.sleep(2000);                 // ✗ gu--essed timing
assertThat(repo.findById(id)).isPresent();

Rule of thumb: Keep integration tests few, isolated, cache-friendly, and on a real database — and poll for async results instead of sleeping.

A common convention names fast unit tests *Test and slower integration tests *IT (or *ITCase), then wires the build to run them in different phases so the quick feedback loop stays quick.

<!-- Maven: Surefire runs *Test (unit), Failsafe runs *IT (integration) -->
<plugin><artifactId>maven-surefire-plugin</artifactId></plugin>   <!-- mvn test -->
<plugin><artifactId>maven-failsafe-plugin</artifactId></plugin>   <!-- mvn verify -->
// Gradle: a separate source set / task for integration tests
tasks.register('integrationTest', Test) { /* runs *IT */ }

Developers run mvn test (or ./gradlew test) constantly for sub-second feedback; the heavier *IT suite (Testcontainers, full context) runs on verify/CI. JUnit @Tag can also partition tests.

Rule of thumb: Separate fast *Test from slow *IT so the everyday loop runs only the quick tests and the full suite runs on CI.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel