Spring Boot Test Stack: @SpringBootTest, @WebMvcTest, @DataJpaTest, @MockBean

Master Spring Boot testing annotations: compare @SpringBootTest vs @WebMvcTest vs @DataJpaTest, and learn @MockBean for effective unit testing.

published: reading time: 29 min read author: GeekWorkBench
Quick Summary

Master Spring Boot testing annotations: compare @SpringBootTest vs @WebMvcTest vs @DataJpaTest, and learn @MockBean for effective unit testing. The guide uses practical examples to explain introduction to spring boot testing stack, when to use and when not to use each annotation and shows how to apply the ideas in a Spring Boot project.

Spring Boot Test Stack: @SpringBootTest, @WebMvcTest, @DataJpaTest, @MockBean

Testing is the backbone of any production-grade application, and Spring Boot gives you a testing ecosystem that covers every layer of your app. Rather than spinning up the full application context for every test, Spring Boot provides targeted annotations that load only the components you actually need. This means faster feedback cycles and more focused tests that catch specific categories of bugs.

This guide covers the four most essential testing annotations in the Spring Boot arsenal: @SpringBootTest, @WebMvcTest, @DataJpaTest, and @MockBean. You will see when to reach for each one, how they differ internally, and the practical trade-offs that determine which fits your scenario best.

Introduction to Spring Boot Testing Stack

Spring Boot’s testing support builds on JUnit 5 and the Spring Test framework, with a layered approach that mirrors the architecture of the application itself. The framework ships with dedicated test annotations that optimize context loading based on what you are actually testing.

Each annotation targets a specific testing scenario. @SpringBootTest is the heavyweight option that bootstraps your entire application context, including all beans and configurations. @WebMvcTest narrows the scope to web layer components, loading only the controller slice and mock MVC infrastructure. @DataJpaTest focuses on the persistence layer, setting up an in-memory database and repository beans. @MockBean works alongside any of these to replace specific beans with Mockito mocks, giving you control over dependencies without touching the real implementation.

The testing stack also works with AssertJ for fluent assertions, Mockito for test doubles, and Spring’s own TestRestTemplate or MockMvc for HTTP-level testing. Getting comfortable with how these pieces work together is the foundation for writing tests that are both meaningful and maintainable.

When to Use and When Not to Use Each Annotation

Picking the right annotation comes down to what you are trying to verify. The wrong choice leads to slow tests, confusing failures, or coverage gaps.

@SpringBootTest

Reach for @SpringBootTest when you need to test multiple layers working together, such as an end-to-end flow where a controller calls a service that queries a repository. It is the right tool for integration tests that span several components, or when you want to validate that your auto-configuration behaves correctly in a production-like environment.

Do not use @SpringBootTest for pure unit tests that only care about a single class’s logic. Loading the full context is slow, and when failures happen, the stack trace spans the entire application, making them harder to diagnose. Save it for tests that genuinely need the full stack.

@WebMvcTest

Reach for @WebMvcTest when you want to test controller logic, request mapping, validation, and response rendering without the service and persistence layers in the picture. It is well-suited for verifying HTTP contracts, JSON serialization, and input validation without any database involvement.

Do not use @WebMvcTest when you need to test business logic in service classes, or when your controller depends on complex service interactions that are hard to mock cleanly at that boundary. Note that @WebMvcTest does not load @Service beans, so if your controller delegates to a service, you must provide a mock explicitly.

@DataJpaTest

Reach for @DataJpaTest when you need to verify that your JPA entities, repositories, and query methods work correctly against a real database. It sets up an embedded database, wraps each test in a transaction that rolls back automatically, and loads only the persistence-related components.

Do not use @DataJpaTest for business logic, web layer behavior, or anything that does not involve database interactions. Also note that @DataJpaTest does not load your application configuration by default, so Flyway migrations or custom DataSource settings may not be active unless you explicitly enable them.

@MockBean

Reach for @MockBean when you need to replace a dependency with a controlled mock to isolate the code under test. It works with any Spring-aware annotation and shines when the real implementation is slow, non-deterministic, or difficult to set up in a test environment.

Do not overuse @MockBean to mock half the application. Tests that mock everything pass easily but stop reflecting real behavior, giving you a false sense of security. Use mocks to eliminate dependencies, not to simulate the entire system.

Test Pyramid Architecture

The Spring Boot testing annotations map naturally to the test pyramid principle. Unit tests at the base verify individual class behavior, integration tests in the middle verify layer interactions, and end-to-end tests at the top verify complete user workflows. Each annotation corresponds to a different level of this pyramid.

graph TD
    A[End-to-End Tests<br>@SpringBootTest] --> B[Integration Tests<br>@WebMvcTest / @DataJpaTest]
    B --> C[Unit Tests<br>Plain JUnit + Mockito]
    C --> D[Fast Execution]
    D --> C
    B --> A
    A --> E[Full Context Loading]
    E --> A
    B --> F[Sliced Context Loading]
    F --> B

Failure Scenarios

Knowing how these tests fail in practice helps you debug issues quickly when they surface.

Context Cache Collision

When multiple test classes use @SpringBootTest with different configurations, Spring’s context caching can serve a cached context that does not match your test’s expectations. This shows up as obscure bean creation failures or missing beans. The fix is to use @DirtiesContext to reset the cache between test classes, or to ensure that unique configurations declare unique profiles.

Mock Bean Not Injected

Using @MockBean on a type that is not part of the loaded context results in a silent failure. The mock is created but never injected. The test runs, but assertions fail because the real bean is still being used. Always check that the type you are mocking is actually present in the context your annotation loads.

@DataJpaTest Database Transaction Isolation

Tests using @DataJpaTest are transactional by default, with changes rolled back after each test. If you manually commit a transaction or use @Transactional(propagation = Propagation.NOT_SUPPORTED), you break this isolation and may cause state to leak between tests. Keep your test setup within the transactional boundaries, or explicitly disable them if you need non-transactional behavior.

Trade-off Comparison

Aspect @SpringBootTest @WebMvcTest @DataJpaTest
Context scope Full application context Web layer only (controllers, filters, advice) Persistence layer only (entities, repositories)
Startup time Slow (full bootstrap) Fast (sliced context) Fast (embedded DB)
Database Real database or test configuration No database Embedded in-memory DB
Service beans Loaded Not loaded (must mock) Not loaded (must mock)
Repository beans Loaded Not loaded Loaded
Ideal for Integration and smoke tests Controller unit tests Repository and query tests
Configuration Uses all @Configuration classes Uses only web-related configs Uses only JPA-related configs
Mocking needs Minimal (real collaborators) Required for services Required for services

Implementation Examples

@SpringBootTest with TestRestTemplate

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturnUserWhenValidIdProvided() {
        ResponseEntity<User> response = restTemplate.getForEntity("/api/users/1", User.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().getId()).isEqualTo(1L);
    }
}

This test loads the full application context on a random port and uses TestRestTemplate to make real HTTP requests. The RANDOM_PORT setting is preferred because it avoids port conflicts when tests run in parallel.

@WebMvcTest with MockMvc

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void shouldReturnUserWhenServiceReturnsValidUser() throws Exception {
        User testUser = new User(1L, "alice", "alice@example.com");
        when(userService.findById(1L)).thenReturn(testUser);

        mockMvc.perform(get("/api/users/1")
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.username").value("alice"));
    }
}

@WebMvcTest loads only the web layer components, keeping tests fast and focused. The @MockBean annotation replaces the UserService with a Mockito mock, so you can control its behavior without needing the actual service implementation.

@DataJpaTest with Embedded Database

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldFindUserByEmail() {
        User savedUser = userRepository.save(new User(null, "bob", "bob@example.com"));

        User foundUser = userRepository.findByEmail("bob@example.com");

        assertThat(foundUser).isNotNull();
        assertThat(foundUser.getUsername()).isEqualTo("bob");
    }

    @Test
    @Sql(statements = "INSERT INTO users (id, username, email) VALUES (100, 'carol', 'carol@example.com')")
    void shouldFindUserWithSqlScript() {
        User foundUser = userRepository.findByEmail("carol@example.com");

        assertThat(foundUser).isNotNull();
        assertThat(foundUser.getUsername()).isEqualTo("carol");
    }
}

@DataJpaTest sets up an in-memory embedded database by default, which is fast and requires no external setup. The @AutoConfigureTestDatabase(replace = Replace.NONE) annotation tells Spring to use your configured test database instead of the default embedded one.

@MockBean for Service Isolation

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.kafka.core.KafkaTemplate;

import static org.mockito.Mockito.*;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
class OrderServiceKafkaTest {

    @MockBean
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    @Autowired
    private OrderService orderService;

    @Test
    void shouldSendKafkaMessageWhenOrderPlaced() {
        Order order = new Order(1L, "SKU-123", 2);
        when(kafkaTemplate.send(anyString(), any(OrderEvent.class)))
                .thenReturn(mock(completionFuture.class));

        orderService.placeOrder(order);

        verify(kafkaTemplate, times(1)).send(eq("order-events"), any(OrderEvent.class));
    }
}

@MockBean bridges Mockito and the Spring application context, letting you mock Spring-managed beans. This is especially useful for external dependencies like message brokers, remote services, or anything expensive to initialize in a test environment.

Observability Checklist

When writing integration tests with these annotations, keep observability requirements in mind.

  • Verify that distributed tracing headers propagate correctly through tests that make HTTP calls.
  • Confirm that metrics captured during test execution are distinguishable from production metrics.
  • Ensure that log output during tests can be correlated to specific test methods.
  • Validate that health check endpoints respond correctly in the test environment.
  • Check that custom actuator metrics registered by your application are available in tests.

Security Notes

Testing security needs special attention because test configurations often differ from production setups.

  • @WebMvcTest does not load Spring Security by default. Use @WebMvcTest(controllers = UserController.class, secure = true) or explicitly include your security configuration if you need to test security filters.
  • Do not hardcode credentials in test configurations. Use @TestConfiguration beans or environment-specific property sources to inject secrets.
  • Make sure @DataJpaTest tests do not inadvertently expose data through test entity seeding that resembles production data.
  • Verify that mock objects in tests do not accidentally disable security checks in ways that would not happen in production.

Common Pitfalls

These mistakes appear frequently when teams adopt these annotations.

Over-mocking with @SpringBootTest. Teams sometimes use @MockBean extensively within @SpringBootTest tests, effectively mocking the entire application and defeating the purpose of an integration test. Reserve @SpringBootTest for scenarios where you want real collaborators.

Incorrect context caching assumptions. Spring caches application contexts across test classes, but only when the context configuration is identical. Adding a new bean or configuration to one test class can corrupt the cache for subsequent tests if they expect the original configuration. Use @DirtiesContext to control when the cache is invalidated.

Ignoring @DataJpaTest transaction behavior. Because @DataJpaTest wraps each test in a transaction that rolls back after completion, any data persisted within a test is not visible to subsequent tests. If you need to share data across tests, apply @Transactional at the class level and manage rollback behavior explicitly.

Using @WebMvcTest for service logic. Controllers in @WebMvcTest receive mock services, but the service layer logic itself is not tested. If your business logic is complex, write separate unit tests for the service class using plain JUnit and Mockito, and reserve @WebMvcTest purely for the controller contract.

Trade-Off Table

Here is how the three annotations compare across the dimensions that matter most in practice.

Dimension @SpringBootTest @WebMvcTest @DataJpaTest
Context scope Full application context Web layer only (controllers, advice, filters) Persistence layer only (entities, repositories)
Startup time Slow (full bootstrap, all beans) Fast (sliced web context) Fast (embedded DB, minimal context)
Database Real database or test-configured No database Embedded in-memory DB (H2 by default)
Service beans All loaded Not loaded (must supply @MockBean) Not loaded (must supply @MockBean)
Repository beans All loaded Not loaded Loaded
Controller beans All loaded Loaded Not loaded
Security Full security config Not loaded by default (opt-in) Not loaded
Ideal use case End-to-end integration, smoke tests Controller contract, JSON serialization JPA queries, entity mapping, repository logic
Mocking required Minimal (real collaborators) Yes, for service layer Yes, for service layer
Transaction behavior Test-managed or real transactions No transaction semantics Automatic rollback after each test
Configuration classes All @Configuration classes Only web-related configs Only JPA-related configs
External dependencies Real (Kafka, REST, DB) Mocked or unavailable Mocked or unavailable
Feedback cycle speed Slow (~seconds per test) Fast (~milliseconds per test) Fast (~milliseconds per test)

Bookmark this for when you need to pick the right annotation, and check back as your project changes.

Quick Recap Checklist

  • Use @SpringBootTest for end-to-end integration tests that span multiple layers.
  • Use @WebMvcTest for focused controller tests without service or persistence layers.
  • Use @DataJpaTest for repository and JPA query validation with an embedded database.
  • Use @MockBean to replace specific dependencies with mocks in any test annotation.
  • Configure @WebMvcTest with security if your controllers use Spring Security.
  • Remember that @DataJpaTest is transactional and rolls back after each test.
  • Avoid over-mocking in @SpringBootTest tests to preserve integration coverage.
  • Use @DirtiesContext when test configurations modify the shared context cache.
  • Validate that observability tools (tracing, metrics, logging) work correctly in tests.
  • Keep credentials out of test code; use environment variables or test configuration sources.

Interview Questions

1. What is the difference between @SpringBootTest and @WebMvcTest, and when would you choose one over the other?

@SpringBootTest loads the entire Spring application context, including all beans in your application. @WebMvcTest loads only the web layer components, specifically controllers, @ControllerAdvice classes, and web-related configurations.

Pick @SpringBootTest when you need to verify that multiple layers work together correctly, such as an end-to-end flow where a controller calls a service that queries a repository. Pick @WebMvcTest when you want to test only the controller layer in isolation, checking request mapping, response serialization, and validation without the overhead of loading the full application context.

@WebMvcTest is significantly faster because it skips the persistence layer, message brokers, and other infrastructure that a full context load requires. If your test suite runs slowly, switching some @SpringBootTest tests to @WebMvcTest is often the first thing to try.

2. How does @DataJpaTest work and what database configuration does it use by default?

@DataJpaTest creates a sliced test context that focuses on JPA components. By default, it replaces any DataSource with an embedded in-memory database, typically H2, and configures EntityManagerFactory, TransactionManager, and repository beans. If Flyway or Liquibase are on the classpath, JPA auto-configuration may activate them depending on your setup.

Each test method runs inside a transaction that rolls back automatically after the test finishes. You do not need to manually clean up database state between tests. This keeps tests independent and prevents one test's data from leaking into the next.

If you need to use your actual test database instead of an embedded one, annotate with @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) to prevent the DataSource from being replaced.

3. What is the purpose of @MockBean and what are the risks of overusing it?

@MockBean connects Mockito with the Spring application context by replacing a Spring-managed bean with a mock. This lets you control how a dependency behaves without needing the real implementation, which is useful for cutting out slow or non-deterministic collaborators like remote services, databases, or message brokers.

The danger of overusing it is that it hollows out integration tests. If you mock every dependency inside @SpringBootTest, you end up with a test that runs a single class's code path while everything else is stubbed. The tests are fast and green, but they do not catch bugs that span multiple real components.

Use @MockBean sparingly and deliberately. Mock external systems like Kafka or external REST APIs. For internal collaborators like services and repositories, prefer using the real thing when you are in a @SpringBootTest.

4. How does Spring's test context caching work, and what causes cache misses or collisions?

Spring caches application contexts across test classes to avoid rebuilding the context for every test. The cache key is derived from the configuration: the set of configuration classes, active profiles, environment variables, and test property sources. If two test classes produce the same key, they share the same context instance.

Cache collisions happen when tests have slightly different configurations that Spring does not recognize as distinct. For example, adding a new @Bean method to a configuration class after another test has cached the context does not automatically invalidate that cache. One test then gets a context that is missing the new bean, leading to confusing "bean not found" failures despite the bean being clearly defined.

@DirtiesContext marks contexts as dirty after a test, forcing Spring to rebuild the cache entry. You can apply it at the class level, method level, or around a specific test phase depending on how drastically the test changes the context.

5. Describe how you would test a controller that depends on a service, and what are the trade-offs between testing the controller in isolation versus testing the full stack.

To test a controller in isolation, annotate the test class with @WebMvcTest and use @MockBean to provide a mock service. The test then uses MockMvc to fire requests and verify responses, focusing entirely on the web contract without touching service or database logic.

The trade-off is that this approach does not catch bugs in the service layer or anything downstream from the controller. You also have to maintain mock configurations that can become outdated if the service interface changes without updating the test.

Testing the full stack with @SpringBootTest loads the real service and repository beans, so you catch genuine integration issues. The cost is slower test execution and harder failure diagnosis when something goes wrong. A practical approach is to use @WebMvcTest for fast, targeted controller contract tests and reserve @SpringBootTest for a smaller set of critical end-to-end scenarios that cover the major integration points.

6. How does Spring's context caching mechanism work across test classes, and what scenarios cause cache invalidation?

Spring caches application contexts using a cache key derived from the configuration tuple: annotated classes, active profiles, property sources, and environment variables. When two test classes produce identical cache keys, Spring reuses the same context instance rather than rebuilding it, which significantly speeds up test suites.

Cache invalidation triggers in several scenarios: when @DirtiesContext is applied (marking the context dirty after a test), when configuration classes change between test runs, when a different active profile is used, or when test property sources differ. A subtle issue arises when a test class adds a new bean definition after another test class has already cached the context—the original cached context will not include the new bean, leading to confusing "NoSuchBeanDefinitionException" failures.

Best practice is to group tests by their context configuration and use @DirtiesContext strategically when tests modify the context state.

7. What is the difference between @MockBean and Mockito's @Mock annotation, and when would you prefer one over the other?

Mockito's @Mock creates a plain mock object outside of Spring's context—it is a pure unit test technique that works with new Mock() but has no awareness of the Spring application context. @MockBean from Spring Boot Test does the same thing but registers the mock into the Spring context, replacing the real bean of that type everywhere it is injected.

Use Mockito's @Mock when you are writing pure unit tests with no Spring context at all, such as testing a service class in isolation with new Service(new MockRepository()). Use @MockBean when you need the mock to be injected by Spring into @Autowired fields, meaning the class under test is participating in a sliced Spring test context like @WebMvcTest or @DataJpaTest.

The key distinction is context integration: @MockBean is Spring-aware and participates in dependency injection, while @Mock is purely a Mockito construct for standalone test doubles.

8. How does @DataJpaTest handle transactions, and what happens if you need a test to run outside of the default transactional boundary?

@DataJpaTest wraps every test method in a transaction by default, with automatic rollback after the test completes. This means each test sees a clean database state with no data leaking to subsequent tests. The @Transactional annotation is applied implicitly by the annotation itself.

If you need a test to run outside the transactional boundary—perhaps to verify how your code behaves under explicit transaction commit or to test transaction propagation— you can override the default by removing @Transactional on a specific test method, or by using @Transactional(propagation = Propagation.NOT_SUPPORTED) to explicitly suspend the transaction. However, this breaks the isolation guarantee and requires manual cleanup of any persisted data to avoid side effects on other tests.

Another approach is to use @Sql to set up a known database state before a test that intentionally modifies data, knowing the changes will not roll back.

9. What is test slicing in Spring Boot, and which other @...Test annotations provide sliced contexts?

Test slicing refers to Spring Boot's mechanism of loading only a subset of the application context for a test, targeting a specific layer or concern. Instead of bootstrapping the full application, a sliced test only registers the beans relevant to the slice, dramatically reducing startup time and reducing the surface area being tested.

Beyond @WebMvcTest and @DataJpaTest, Spring Boot provides several other sliced annotations: @JsonTest loads only JSON serialization components; @RestClientTest loads MockRestServiceServer for REST client testing; @JdbcTests or @JooqTests for JDBC-specific context; and @BootstrapWith can be customized to compose your own slices. Each one replaces or filters beans unrelated to the target concern.

Choosing a sliced annotation is appropriate when you can isolate your test to a single concern. If your test needs to verify interactions between layers, use a broader annotation or @SpringBootTest.

10. How do you test asynchronous code that uses @Async or CompletableFuture in a Spring Boot test context?

Testing async code requires special handling because the main thread does not wait for the async operation to complete. With @SpringBootTest, you can use AsyncAssertions from AssertJ (assertThatfuture) or Awaitility to wait for the async result. Alternatively, @WebMvcTest can mock the async executor to make the async result immediately available within the test thread.

For @Async methods, consider using @MockBean on the TaskExecutor bean to execute the async task synchronously within the test context. For CompletableFuture chains, you can simply call .get() on the future within your test to block until the result is ready, or use assertThatFuture() from AssertJ for a non-blocking assertion style.

Do not assume async methods have completed immediately after the calling code returns—the executor processes them on separate threads, so assertions must account for the asynchronous nature.

11. What is the purpose of @TestPropertySource and how does it differ from application-test.properties?

@TestPropertySource is an annotation that declares property sources for a test class or method, directly inline in the test code. It takes precedence over the main application properties and can be used to override specific beans, connection strings, or feature flags without affecting the broader test suite.

application-test.properties works at the resource bundle level and is automatically picked up when the test profile "test" is active, but it applies to all tests in that suite. @TestPropertySource gives you finer-grained control, targeting specific test classes or even individual test methods with different property values.

A common pattern is to use @TestPropertySource on specific test classes that need unusual configurations, while keeping application-test.properties for shared test defaults like database connection details and mock server ports.

12. How would you test a Spring Security configuration in a @WebMvcTest context?

@WebMvcTest does not load Spring Security by default. To test security behavior, you have two main approaches. First, you can explicitly include your security configuration: @WebMvcTest(controllers = UserController.class, secure = true) or by importing the security configuration class directly using @Import(MySecurityConfig.class).

Second, you can use Spring Security's test support with @WithMockUser, @WithAnonymousUser, or @WithUserDetails to simulate different security principals without actually authenticating. This lets you test role-based access rules, CSRF token handling, and authentication requirements at the controller level.

For integration-level security testing with @SpringBootTest, TestRestTemplate or MockMvc with Spring Security's request post-processors let you fire requests as specific users and verify the security behavior end-to-end.

13. What are the trade-offs between using an embedded in-memory database like H2 versus a real database in tests?

Embedded databases like H2 offer speed and zero-setup requirements—tests spin up an in-memory database in milliseconds and clean state automatically between runs. They are ideal for CI pipelines where installing a database server is impractical. However, H2's SQL dialect sometimes differs from production databases like PostgreSQL or MySQL, meaning query syntax, type handling, or behavior differences may not surface until production.

Real databases (testcontainers, local Docker, or a dedicated test instance) provide fidelity—you test against the actual database your application will use in production, catching genuine incompatibilities early. The cost is slower test startup and more complex infrastructure requirements.

A practical compromise is to use H2 for unit and integration tests during development (fast feedback) and run a subset of tests against the real database in CI to catch dialect-specific issues.

14. How does @AutoConfigureTestDatabase work and when would you need to replace or disable it?

@AutoConfigureTestDatabase is a Spring Boot auto-configuration that automatically replaces any configured DataSource with an embedded or test-specific database unless you tell it otherwise. It detects H2, Derby, or other embedded databases on the classpath and configures them automatically.

You would disable or override it when you want tests to use your actual test database instead of an embedded one. Using @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) prevents Spring from replacing your DataSource, so your application-test.properties or @TestPropertySource database settings take effect.

This is particularly important when your repository tests need to exercise Flyway migrations, custom column types, or stored procedures that only exist in your real database schema, not in the simplified embedded schema.

15. What is the difference between @SpringBootTest(WEB_ENVIRONMENT = RANDOM_PORT) and DEFINED_PORT, and when does each matter?

DEFINED_PORT uses a fixed port (default 8080) for the test server, which can cause conflicts when tests run in parallel on the same machine. RANDOM_PORT allocates an available port at runtime, eliminating port collision issues and making parallel test execution safe by default.

MOCK_ENVIRONMENT uses a mock web layer without starting a real server at all, relying on MockMvc internally. NONE skips the web environment entirely, loading only the application context without any HTTP server.

For integration tests that use TestRestTemplate, RANDOM_PORT is almost always the right choice because it avoids flaky failures from port conflicts. For pure controller tests, prefer @WebMvcTest which is cleaner and faster without the network overhead.

16. How do you test custom Spring Boot auto-configuration classes in isolation?

Custom auto-configuration classes can be tested using @SpringBootTest with @Import to pull in only the auto-configuration under test, or by directly testing the configuration bean definitions using a minimal context. A common pattern is to create a test configuration class that imports the auto-configuration and verifies the expected beans are registered.

For deeper isolation, you can test the auto-configuration class itself using standard JUnit without Spring, instantiating the @Configuration class directly and verifying that the bean definitions it declares are correct. This validates the configuration logic without the overhead of a Spring context.

When the auto-configuration depends on conditional annotations like @ConditionalOnClass or @ConditionalOnProperty, use @MockBean to supply missing classes or properties so the auto-configuration can be activated in the test context.

17. What is the role of @BootstrapWith and how can you customize the test context hierarchy?

@BootstrapWith is a class-level annotation that controls how the Spring TestContext Framework bootstraps the application context. The default Bootrapper is SpringBootTestContextBootstrapper, which handles @SpringBootTest and its configuration. You can substitute a custom Bootstrapper to change how contexts are built and cached.

Test context hierarchy means that nested @Nested test classes can have their own context configuration that inherits from the parent context. By default, nested tests share the parent's context unless they declare a different @BootstrapWith or @ContextConfiguration, which allows you to build more granular test scenarios without rebuilding the full application context for each nested class.

Customizing the bootstrapper is rare but useful when building proprietary test slices or integrating with frameworks that need special context initialization.

18. How would you test a @Scheduled task in a Spring Boot test context?

@Scheduled tasks run on a TaskScheduler thread pool outside the normal request lifecycle, which makes them tricky to test synchronously. In a @SpringBootTest context, you can use @MockBean on the TaskScheduler or ScheduledExecutorService to spy on submitted tasks without waiting for their execution.

For more direct testing, you can inject the TaskScheduler into your test and manually trigger scheduled tasks, or use Awaitility to wait for assertions until the scheduled task completes on its own thread. Another approach is to temporarily disable scheduling in tests using @EnableScheduling on a mock configuration and trigger tasks manually.

Avoid relying on timing-based assertions in a real scheduling context—use mocks or direct invocation to make tests deterministic and independent of executor timing.

19. What are the advantages of using @Sql compared to data.sql for test data setup?

@Sql is an annotation-driven approach that lets you declare SQL scripts at the method or class level, giving you fine-grained control over when scripts run relative to individual tests. It supports both setup scripts (run before the test) and teardown scripts (run after), and can be applied selectively to specific test methods.

data.sql runs automatically every time the application context is refreshed, which means it executes for every test class using a given context, even tests that do not need that data. It is also harder to control per-method—it is an application-wide mechanism rather than a per-test mechanism.

@Sql is the better choice when different test methods or classes need different data setups, and you want explicit control over execution order and cleanup. data.sql is appropriate for static reference data that applies broadly across your test suite.

20. How does @Nested help with test organization, and what are the context inheritance rules for nested tests in Spring?

@Nested from JUnit 5 allows you to logically group related tests inside a test class, improving readability and test report organization. From Spring's perspective, @Nested test classes can participate in the Spring TestContext Framework, meaning they can use @Autowired, @MockBean, and other Spring test annotations.

By default, nested test classes inherit the parent test's Spring context. This means each nested class does not trigger a full context reload—they reuse the parent's context, making nested tests with Spring annotations efficient. However, this also means context modifications (like adding a @MockBean) in an outer test class affect nested sibling tests if they share the same context.

Use @DirtiesContext on nested classes if they modify the context state in ways that should not leak to siblings, ensuring test isolation even within the same class hierarchy.

Further Reading

Conclusion

Spring Boot’s testing stack gives you a layered approach that scales from fast unit tests to full integration tests. @MockBean lets you isolate slices without real infrastructure, @DataJpaTest gives you a scoped context with an in-memory database, and @SpringBootTest with a random port is the gold standard for truly integration-level verification. Testcontainers extends that to real database and middleware containers in CI, closing the gap between “it works on my machine” and “it works in production.”

Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path