MockMvc vs WebTestClient, testing secured endpoints with MockMvc and simulated users/roles, testing filters and interceptors, PATCH requests, jsonPath assertions, @WebMvcTest vs full context, file uploads with MockMvc, WebTestClient with reactive and streaming (Flux/SSE) endpoints, and Testcontainers — what it solves vs H2, spinning up Postgres/MySQL, reusing containers across classes, Kafka/RabbitMQ containers, overriding properties with container ports (@ServiceConnection), dynamic tests, Docker detection, Flyway/Liquibase integration, CI limitations, and Maven/Gradle lifecycle integration.
Published September 25, 2026
These are hands-on questions. Show working snippets, and explain the speed and fidelity trade-offs: MockMvc (fast, in-process) vs a real server, and Testcontainers (realistic) vs embedded fakes.
MockMvc and WebTestClient?Short answer:
MockMvc: for Spring MVC (servlet). It runs in-process through a mock servlet request and response, with no network, and uses the ResultMatcher style (andExpect). Boot 3.4 adds MockMvcTester (AssertJ).WebTestClient: a fluent, non-blocking test client:
bindToApplicationContext, bindToController, bindToRouterFunction);bindToServer, or auto-configured with RANDOM_PORT);MockMvcWebTestClient);returnResult(...).getResponseBody() with StepVerifier).Choose MockMvc for MVC slice tests, and WebTestClient for WebFlux, real-server tests, or when you want one client style across stacks.
Short answer: With @WebMvcTest, the security filter chain is included (your SecurityFilterChain configuration, when imported; Boot applies it to the slice). Then:
@WebMvcTest(AdminController.class)
@Import(SecurityConfig.class)
class AdminSecurityTest {
@Autowired MockMvc mvc;
@MockitoBean AdminService service;
@Test void anonymousGets401() throws Exception {
mvc.perform(get("/api/admin/users")).andExpect(status().isUnauthorized());
}
@Test @WithMockUser(roles = "USER")
void userWithoutAdminRoleGets403() throws Exception {
mvc.perform(get("/api/admin/users")).andExpect(status().isForbidden());
}
@Test void adminWithJwtScopeGets200() throws Exception {
mvc.perform(get("/api/admin/users").with(jwt().authorities(new SimpleGrantedAuthority("ROLE_ADMIN"))))
.andExpect(status().isOk());
}
@Test void postNeedsCsrfForSessionAuth() throws Exception {
mvc.perform(post("/admin/form").with(user("admin").roles("ADMIN")).with(csrf())).andExpect(status().is3xxRedirection());
}
}
The simulation options are @WithMockUser, @WithUserDetails, custom @WithSecurityContext annotations, and the post-processors user(), jwt(), oauth2Login(), opaqueToken().
Short answer: Yes.
WebMvcConfigurer are part of the MVC slice, and run in MockMvc automatically.@WebMvcTest and @AutoConfigureMockMvc add Spring-managed filter beans (including Spring Security's filter chain) to MockMvc. With a manual standalone setup, add them explicitly: MockMvcBuilders.standaloneSetup(controller).addFilters(new CorrelationIdFilter()).addInterceptors(...).DispatcherType.ERROR flows) needs a real server test.Short answer:
mvc.perform(patch("/api/products/{id}", ID)
.contentType("application/merge-patch+json") // or application/json-patch+json for RFC 6902
.header(HttpHeaders.IF_MATCH, "\"v7\"") // optimistic concurrency via ETag
.content("""{"price": 1299.00, "description": null}""")
.with(jwt()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.price").value(1299.00))
.andExpect(jsonPath("$.description").doesNotExist());
Test these semantics:
null clears a field (merge-patch semantics);jsonPath()?Short answer:
.andExpect(jsonPath("$.id").value(ID.toString()))
.andExpect(jsonPath("$.lines", hasSize(2)))
.andExpect(jsonPath("$.lines[0].sku").value("SKU-1"))
.andExpect(jsonPath("$.lines[*].qty", everyItem(greaterThan(0))))
.andExpect(jsonPath("$.total").value(closeTo(250.0, 0.001)))
.andExpect(jsonPath("$.internalCost").doesNotExist()) // guard against leaking fields
.andExpect(jsonPath("$.status", is(oneOf("PAID", "PENDING"))))
For whole-document comparisons, use content().json(expectedJson, JsonCompareMode.LENIENT) (JSONassert), and use strict mode when extra fields must fail. Use @JsonTest slices to test serialisation rules separately (JacksonTester).
@WebMvcTest and testing with the full context?Short answer:
@WebMvcTest: only the web layer. Services are mocked. It's fast (a small context), with focused failures, but it can't catch wiring or transaction problems, or real serialisation of data coming from the database.@SpringBootTest + MockMvc, or a real server): real services, repositories and configuration, catching integration bugs. It's slower, needs infrastructure (Testcontainers), and failures are harder to localise.Use both: many slice tests for the API contract and the error mapping, and fewer full-context tests for critical flows.
Short answer: Yes, with multipart(...) and MockMultipartFile parts (see the upload example in the previous lesson). Also test:
spring.servlet.multipart.max-file-size in the test properties);PUT multipart, use multipart(HttpMethod.PUT, url) (Spring 5.3.22+).For streaming large uploads or real container limits, test against a real server.
WebTestClient handle reactive endpoints? How do you test streaming (Flux) responses?Short answer: WebTestClient subscribes to the reactive response, and supports:
expectBody(Dto.class), expectBodyList(...);returnResult(Dto.class).getResponseBody(), which gives a Flux that you verify with StepVerifier: take N elements, check them, and cancel (important for infinite SSE streams);Accept: text/event-stream, and asserting the Content-Type.Flux<PriceTick> ticks = client.get().uri("/prices/stream?symbol=INFY")
.accept(MediaType.TEXT_EVENT_STREAM).exchange()
.expectStatus().isOk()
.returnResult(PriceTick.class).getResponseBody();
StepVerifier.create(ticks.take(3))
.expectNextMatches(t -> t.symbol().equals("INFY"))
.expectNextCount(2)
.verifyComplete();
Use virtual time (StepVerifier.withVirtualTime) for interval-based publishers, to avoid real waits.
Short answer: Fidelity. Testcontainers starts real dependencies in Docker, such as the same database engine and version as production, Kafka, RabbitMQ, Redis, Elasticsearch, LocalStack (AWS) and Keycloak, managed from the test code (started before tests, and cleaned up automatically by the Ryuk sidecar). Compared with H2 or other embedded fakes, you catch:
The costs: it needs Docker, the containers take seconds to start (use reuse or singletons), and it uses resources in CI.
Short answer: Boot 3.1+ uses @ServiceConnection, which auto-configures the DataSource (or Kafka, Redis, Mongo…) from the container, with no property plumbing:
@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfig {
@Bean @ServiceConnection
PostgreSQLContainer<?> postgres() { return new PostgreSQLContainer<>("postgres:16-alpine"); }
}
@SpringBootTest
@Import(TestcontainersConfig.class)
class OrderFlowIT { /* the real DataSource points at the container */ }
Before 3.1, or for custom properties, use @DynamicPropertySource:
@Container static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", pg::getJdbcUrl);
r.add("spring.datasource.username", pg::getUsername);
r.add("spring.datasource.password", pg::getPassword);
r.add("app.search.url", () -> "http://" + es.getHost() + ":" + es.getMappedPort(9200)); // mapped random port
}
Containers expose random mapped ports, so always read getMappedPort(...)/getHost(). Never hard-code them.
Short answer:
@TestConfiguration, and start them once per JVM (a static initialiser, or a bean-defined container that the Spring context cache shares). Every test class extending the base shares the container, and context caching keeps one Spring context.@ServiceConnection containers in a shared @TestConfiguration: the container lives as long as the cached context..withReuse(true) plus testcontainers.reuse.enable=true in ~/.testcontainers.properties) keeps containers running between test runs locally, which is great for developer speed. It's not recommended in CI.Short answer:
ConfluentKafkaContainer/KafkaContainer (KRaft mode, no ZooKeeper), with @ServiceConnection wiring spring.kafka.bootstrap-servers. Add Schema Registry containers on a shared Network if you use Avro.RabbitMQContainer, with @ServiceConnection.auto.offset.reset=earliest for test consumers.Short answer: Yes.
@Testcontainers plus @Container fields manage the lifecycle with the JUnit 5 extension:
@TestFactory dynamic tests, the extension's per-method lifecycle doesn't apply to each dynamic test (they share the factory method's lifecycle), so start the containers statically, or manually (container.start() in @BeforeAll, or in the factory), and generate dynamic tests that use them. For example, run the same test suite against several database versions by creating a container per version inside the factory.Short answer: At startup, Testcontainers runs a strategy chain to find a Docker environment:
DOCKER_HOST environment variable, and ~/.testcontainers.properties (docker.host);/var/run/docker.sock), or Windows named pipes;It then pings the Docker daemon through the Docker API, and checks the version and disk space. If none is found, it throws IllegalStateException: Could not find a valid Docker environment. To skip gracefully, use @Testcontainers(disabledWithoutDocker = true), or an assumption on DockerClientFactory.instance().isDockerAvailable().
Short answer: Nothing special is needed. With @ServiceConnection (or dynamic properties) pointing Spring's DataSource at the container, Boot's Flyway or Liquibase auto-configuration runs the real migrations at context startup, against the containerised database, exactly as in production. The best practices:
spring.jpa.hibernate.ddl-auto=validate, to catch entity and schema drift;spring.flyway.locations=classpath:db/migration,classpath:db/testdata);flyway.clean() + migrate() (enable clean only in tests, and never in production).Short answer:
-alpine), use singleton containers, and parallelise forks carefully (each fork starts its own containers, which uses memory and CPU).getHost()/getMappedPort(), and custom networks for container-to-container communication.Short answer:
*Test) in the test phase, and Failsafe runs integration tests (*IT) in integration-test plus verify (it fails the build after cleanup);integrationTest source set and task (or the JVM Test Suite plugin), with check.dependsOn(integrationTest).org.testcontainers:* modules (through the Testcontainers BOM, or Spring Boot's dependency management), plus spring-boot-testcontainers.SpringApplication.from(App::main).with(TestcontainersConfig.class).run() (a TestApplication main class in src/test) runs the application locally with containers, with no local database install needed.Q: Why did @WebMvcTest not load my service bean?
A: Slices only include web components. Services must be mocked (@MockitoBean) or imported explicitly (@Import(MyService.class)). That's by design, to keep slice tests focused and fast.
Q: How do you make container startup faster?
A: Use smaller images, singleton or shared containers, @ServiceConnection (no extra property plumbing), parallel startup (Startables.deepStart), and reusable mode locally. Keep container-based tests in their own suite, so unit tests stay fast.
Q: Is it OK to share one database container among parallel test classes? A: Yes, if the tests isolate their data (unique tenants or IDs, or schema-per-worker). Otherwise, parallel classes interfere. Many teams use one container per fork, plus truncation between classes.
Q: What's LocalStack used for? A: Emulating AWS services (S3, SQS, SNS, DynamoDB, Secrets Manager) in a container, so AWS SDK integrations can be integration-tested without real cloud resources.