MockMvc vs TestRestTemplate, testing exception handlers, form submissions, status/header/body assertions, authenticated endpoints, file upload/download and multipart mocks, content negotiation, jsonPath; @DataJpaTest database setup, loading data and @Sql, testing validation constraints, Testcontainers over H2, custom JPQL queries, lazy loading, schema mismatches, rollback vs persistence across tests, locking scenarios; JWT/OAuth2-protected endpoints, disabling security for tests, profile configs, @TestPropertySource, scheduled/async methods, parallel runs, embedded Kafka, and coverage tools.
Published September 25, 2026
These are "how exactly would you test X" questions. Give concrete code, and mention what the test doesn't cover. A good test proves one behaviour, with realistic infrastructure where the behaviour depends on it.
MockMvc and TestRestTemplate?Short answer:
MockMvc: calls the DispatcherServlet in-process, with no real HTTP server (@WebMvcTest, or @SpringBootTest + @AutoConfigureMockMvc). It's fast, and gives rich assertions (status(), jsonPath(), model()), and it's on the same thread as the test, so @Transactional test rollback applies. It doesn't exercise the servlet container: Tomcat error pages, the actual HTTP connection, compression, and some filter-ordering subtleties.TestRestTemplate (or WebTestClient/RestClient against RANDOM_PORT): makes real HTTP calls to an embedded server. It's the most realistic (serialisation over the wire, the container's behaviour, the whole filter chain). It's slower, and the server-side work runs on other threads (no test rollback).MockMvcTester (Boot 3.4) adds AssertJ-style fluent assertions over MockMvc.Short answer: Use @WebMvcTest (the advice is included in the slice). Make the mocked service throw each exception type, then assert the status and error body:
@WebMvcTest(OrderController.class)
class ErrorHandlingTest {
@Autowired MockMvc mvc;
@MockitoBean OrderService service;
@Test void notFoundMapsTo404ProblemDetail() throws Exception {
given(service.get(ID)).willThrow(new OrderNotFoundException(ID));
mvc.perform(get("/api/orders/{id}", ID))
.andExpect(status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.errorCode").value("ORDER_NOT_FOUND"))
.andExpect(jsonPath("$.detail").value(containsString(ID.toString())));
}
@Test void unexpectedErrorsDoNotLeakDetails() throws Exception {
given(service.get(ID)).willThrow(new IllegalStateException("SQL: select * from secret_table"));
mvc.perform(get("/api/orders/{id}", ID))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.detail").value("Unexpected error"));
}
}
You can also unit-test the advice class directly, or use MockMvcBuilders.standaloneSetup(controller).setControllerAdvice(advice).
@ModelAttribute binding, or form submissions?Short answer: Send form parameters with MockMvc, then assert the binding errors, the model or the redirect:
mvc.perform(post("/register").contentType(MediaType.APPLICATION_FORM_URLENCODED)
.param("email", "not-an-email").param("name", "")
.with(csrf())) // form posts need CSRF when security is on
.andExpect(status().isOk())
.andExpect(view().name("register"))
.andExpect(model().attributeHasFieldErrors("form", "email", "name"));
mvc.perform(get("/orders").param("status", "PAID").param("page", "2")) // query parameters to @ModelAttribute search DTOs
.andExpect(status().isOk());
.andExpect(jsonPath(...)) do?Short answer: Use MockMvc's ResultMatchers:
status().isCreated();header().string("Location", endsWith("/orders/42"));content().contentType(APPLICATION_JSON);content().json("{...}", JsonCompareMode.LENIENT) (JSONassert);jsonPath("$.lines.length()").value(3), jsonPath("$.items[?(@.sku=='A1')].qty").value(hasItem(2)), jsonPath("$.id").exists().jsonPath evaluates a JsonPath expression against the JSON response body, and asserts on the result (with Hamcrest matchers, or .value(...)), so it checks structure and specific fields without matching the whole document. With TestRestTemplate, assert on the ResponseEntity's status, headers and body (with AssertJ, or JSONassert for the body).
Short answer:
@WithMockUser(roles = "ADMIN"), @WithAnonymousUser, and @WithUserDetails("alice") (it loads a real UserDetailsService).mvc.perform(get("/api/orders").with(jwt().jwt(j -> j.subject("u1").claim("scope", "orders.read")))). This bypasses the token decoding, but applies the authorities and claims.oauth2Login(), oidcLogin(), oauth2Client() post-processors.webTestClient.mutateWith(mockJwt()).Short answer: Prefer not to. Testing with security on catches real misconfigurations. If you must:
@WebMvcTest, @AutoConfigureMockMvc(addFilters = false) skips the servlet filters (including security) for pure controller-logic tests;SecurityFilterChain in a @TestConfiguration that permits all (make sure it's only active in tests);@WithMockUser everywhere (keeps security on, and is cleaner).Never add permitAll production configuration behind a "test" profile that could accidentally be activated.
Short answer:
MockMultipartFile file = new MockMultipartFile("file", "invoice.pdf", "application/pdf", pdfBytes);
MockMultipartFile meta = new MockMultipartFile("meta", "", "application/json", """{"orderId":"42"}""".getBytes());
mvc.perform(multipart("/api/documents").file(file).file(meta).with(jwt()))
.andExpect(status().isCreated());
mvc.perform(get("/api/documents/{id}/content", DOC_ID).with(jwt()))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION, containsString("invoice.pdf")))
.andExpect(content().bytes(pdfBytes));
Also test:
max-file-size violation gives 413, or a mapped error);StreamingResponseBody, use asyncDispatch).With a real server, use TestRestTemplate with a MultiValueMap body and FileSystemResource/ByteArrayResource.
Short answer: Send Accept headers, and assert the Content-Type and body format:
mvc.perform(get("/api/orders/1").accept(MediaType.APPLICATION_XML)), then content().contentType(APPLICATION_XML) and xpath("/order/id").string("1");jsonPath(...);Accept gives 406;Content-Type gives 415.Include the XML converter dependency (jackson-dataformat-xml) in the test classpath, if the application supports XML.
@DataJpaTest configure the database? How do you load data before tests? What is @Sql for?Short answer:
Database:
DataSource with an embedded database, and Hibernate's ddl-auto is set to create-drop (unless migrations run, since Flyway and Liquibase run in the slice);@Transactional, with rollback per test;@AutoConfigureTestDatabase(replace = Replace.NONE) plus a Testcontainers database (@ServiceConnection) to test on the real engine.Loading data:
@Sql("/fixtures/orders.sql") runs SQL scripts before (or after) each test, at class or method level, with executionPhase, and @SqlConfig for transaction modes;TestEntityManager.persistAndFlush(...), or repositories in @BeforeEach;src/test/resources/db/testdata);data.sql (applied at startup, and global, so less targeted).Keep fixtures minimal and local to each test for readability.
Short answer:
Validator (fast, with no Spring):Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
var violations = validator.validate(new CreateOrderRequest("", List.of()));
assertThat(violations).extracting(v -> v.getPropertyPath().toString()).containsExactlyInAnyOrder("customerId", "lines");
ConstraintValidators: test them directly, including null handling, or inside ApplicationContextRunner when they have injected dependencies.@WebMvcTest sends invalid payloads, and asserts 400 with the field errors in the Problem Details.@DataJpaTest on the real database (Testcontainers) with flush(), to trigger the constraint and assert DataIntegrityViolationException.Short answer: Yes, and you should, for most integration tests. Testcontainers runs the same database engine and version as production (Postgres, MySQL, Oracle XE, SQL Server) in Docker, so you test:
H2 compatibility modes miss many of these, which gives false confidence. The costs are Docker availability, and startup time (mitigated by singleton or reusable containers and context caching).
Short answer: Use @DataJpaTest on the real database:
flush() and clear() the persistence context, so the query really hits the database.Test pagination (Pageable), and startup validation (a broken JPQL string fails context loading, which the test catches).
Short answer:
Hibernate.isInitialized(entity.getLines()), or PersistenceUnitUtil.isLoaded(entity, "lines"): assert that it's false after a plain findById (lazy works), and true after a fetch-join or entity-graph method.SQLStatementCountValidator, or Hibernate statistics' getPrepareStatementCount()).LazyInitializationException tests: load in one transaction, then access outside it, to prove that your service returns fully initialised DTOs.clear() the persistence context before loading, or you'll get cached instances.Short answer: It depends on the configuration:
ddl-auto=validate, context startup fails with SchemaManagementException (a missing column or table, or a wrong type). That's good: it fails fast;create-drop/update in tests, Hibernate generates a schema from the entities, which hides the mismatch with the real migrations, so tests pass, and production fails;none, errors appear at query time (SQLGrammarException: an unknown column).The best practice: run the real Flyway or Liquibase migrations in tests (on Testcontainers), with ddl-auto=validate, so entity and schema drift is caught in CI.
Short answer:
@Transactional tests), or cleans up (@Sql after, truncation), so tests don't depend on each other's data, and can run in any order or in parallel.@BeforeAll with @TestInstance(PER_CLASS) and a committed transaction, or @Sql at class level with BEFORE_TEST_CLASS, Spring 6.1+). Treat them as immutable.@Commit or @Rollback(false) for a scenario spanning several steps. Better still, put multi-step flows in one test method, or use ordered tests (@TestMethodOrder) sparingly.Short answer:
Optimistic:
EntityManagers (using TransactionTemplate, not the test's transaction);ObjectOptimisticLockingFailureException, and that version incremented once.Also test your retry logic, or the 409 mapping.
Pessimistic:
PESSIMISTIC_WRITE on a row, and hold it (a latch);jakarta.persistence.lock.timeout), and assert PessimisticLockingFailureException/LockTimeoutException, or that B waits until A commits;CountDownLatch/Awaitility to coordinate.Use Testcontainers with the real database, because H2's locking differs.
@TestPropertySource for?Short answer:
@ConfigurationProperties binding and validation: ApplicationContextRunner.withPropertyValues(...), then assert the bound values, or that startup fails on invalid values.@ActiveProfiles("prod") plus assertions on the resolved properties (Environment) or the beans. Check that the profile-specific files load the expected values.@TestPropertySource: adds test-specific property sources (inline properties = "a=b", or locations = "classpath:test.properties") with high precedence over the application's files, for a test class. @SpringBootTest(properties = …) is the Boot equivalent. @DynamicPropertySource is for values computed at runtime.Short answer:
@Async methods: call them through the Spring bean (the proxy), then wait for the result: CompletableFuture.get(timeout), or Awaitility (await().atMost(5, SECONDS).until(() -> repo.count() == 1)). For deterministic unit tests, configure a synchronous executor (SyncTaskExecutor) in a test configuration.@Scheduled wiring: use a short interval in tests, and assert with Awaitility. Or use ScheduledTaskHolder to assert that the task is registered with the expected cron. Disable scheduling in unrelated tests (a conditional @EnableScheduling through a property).Clock, and use a fixed or mutable test clock. Never Thread.sleep for synchronisation.Short answer:
junit-platform.properties:junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
Use @Execution(CONCURRENT)/@ResourceLock to control it. Spring's context cache is thread-safe, and parallel classes can share contexts.
forkCount=1C with reuseForks, Gradle maxParallelForks. Every fork has its own JVM and context cache, so assign isolated infrastructure per fork (Testcontainers per fork, or unique database schemas).Short answer:
@EmbeddedKafka (spring-kafka-test): an in-JVM broker, which is fast and needs no Docker. Point spring.kafka.bootstrap-servers at ${spring.embedded.kafka.brokers}. Produce a record, then assert the listener's side effect (the database row, or a mock invocation) with Awaitility, or consume from the output topic with KafkaTestUtils.getSingleRecord.KafkaContainer/ConfluentKafkaContainer, with @ServiceConnection) gives the real broker behaviour, and the Schema Registry through additional containers.orders.DLT);Short answer:
jacoco-maven-plugin: prepare-agent, report, check with minimum thresholds) or Gradle (jacoco plugin). It reports line, branch, instruction and complexity coverage, as HTML or XML.Key points to cover:
Q: What is Awaitility, and why is it better than Thread.sleep?
A: A library for polling conditions with timeouts (await().atMost(…).untilAsserted(…)). It waits only as long as needed, gives clear failure messages, and avoids flaky fixed sleeps.
Q: How do you test that a transaction rolls back on failure? A: Use an integration test without a test-managed transaction. Call the service with input that fails midway, then query in a new transaction and assert that none of the partial changes persisted.
Q: Should tests use the production application.yml?
A: Mostly yes, so the tests exercise the real configuration, with minimal test overrides (@TestPropertySource, @ServiceConnection), not a completely separate test configuration that drifts from production.
Q: What's a good way to keep test data builders readable?
A: Test data builders or object mothers (anOrder().withStatus(PAID).withLines(2).build()), with sensible defaults, so each test states only what matters to it.