Fix: Spring Boot Test Not Working — ApplicationContext Fails to Load, MockMvc Returns 404, or @MockBean Not Injected
Part of: Java & JVM Errors
Quick Answer
How to fix Spring Boot test issues — @SpringBootTest vs test slices, MockMvc setup, @MockBean vs @Mock, test context caching, and common test configuration mistakes.
The Problem
The test context fails to load:
java.lang.IllegalStateException: Failed to load ApplicationContext
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'userController': Unsatisfied dependency expressed through field 'userService'Or MockMvc returns 404 for routes that exist:
@Test
void testGetUser() throws Exception {
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk());
// Fails: expected 200 but was 404
}Or @MockBean doesn’t inject into the controller:
@MockBean
private UserService userService;
// userService.findById() still calls the real databaseOr tests pass individually but fail when run together:
Tests run: 15, Failures: 3, Errors: 0, Skipped: 0
[WARNING] Tests run in an unexpected orderWhy This Happens
Spring Boot tests have multiple annotations with different scopes, and picking the wrong one is the most common source of confusion. Each annotation loads a different portion of the application context, and the same code can pass or fail depending on which slice you choose.
@SpringBootTestloads the full context — it starts the entire Spring application, including all beans, datasources, and auto-configurations. This is slow and overkill for unit tests.- Test slices load only part of the context —
@WebMvcTestloads only web layer beans;@DataJpaTestloads only JPA components. Using the wrong slice means your beans aren’t loaded, causing404or unsatisfied dependencies. @MockBeanreplaces beans in the Spring context — it’s different from Mockito’s@Mock.@MockBeanregisters a mock in the Spring context;@Mockonly creates a Mockito mock without Spring integration.- Context caching — Spring caches test contexts across tests. Modifying the context (e.g., using
@DirtiesContextor@MockBeanin different tests) creates new contexts and slows test suites.
The second layer of failure is that Spring’s test stack has been rewritten more than once. Boot 1.x, Boot 2.x, and Boot 3.x have meaningfully different test infrastructure, and the answer that works on one major version often breaks on another. Most “Spring Boot test not working” Stack Overflow answers were written before Boot 3 / Spring 6, and the JUnit imports, annotation locations, and javax vs jakarta namespaces have all moved.
Spring Boot Test Version History — What Changed When
- Spring Boot 1.4 (Sept 2016) introduced
@SpringBootTestand the original suite of slice annotations (@WebMvcTest,@DataJpaTest,@JsonTest,@RestClientTest). Before 1.4 you had to wire@ContextConfigurationby hand. - Spring Boot 2.0 (March 2018) dropped JUnit 4 as the default. New projects got JUnit 5 (Jupiter), and
@RunWith(SpringRunner.class)was replaced by@ExtendWith(SpringExtension.class)— or simply implied by@SpringBootTest. Old tutorials that show@RunWithno longer compile on a fresh Boot 3 project. - Spring Boot 2.2 (Oct 2019) made JUnit 5 the default test engine and pulled in AssertJ + Hamcrest + Mockito as default test dependencies via
spring-boot-starter-test. AssertJ’sassertThatbecame the canonical style. - Spring Boot 2.4+ (Nov 2020 onward) stabilized the slice annotations and added
@WebFluxTestimprovements for reactive controllers. The split between blocking (@WebMvcTest) and reactive (@WebFluxTest) became important. - Spring Boot 2.7 (May 2022) marked the start of multiple test-related deprecations: legacy
MockMvc.standaloneSetuppatterns lost first-party recommendation in favour of@WebMvcTest, and config metadata for test properties was tightened. - Spring Boot 3.0 / Spring Framework 6 (Nov 2022) is the big break. Boot 3 requires Java 17, switches
javax.*→jakarta.*(sojavax.persistencebecomesjakarta.persistencein@DataJpaTestentities), and removes Boot 1 / Boot 2 deprecations. If your test importsjavax.servlet.http.HttpServletRequest, it will not compile on Boot 3. - Spring Boot 3.1 (May 2023) introduced first-class Testcontainers integration via
@ServiceConnectionand thespring.testcontainers.beans.startupproperty —@DynamicPropertySourceis no longer required for the common databases. TheConnectionDetailsAPI replaces hand-wired JDBC URLs. - Spring Boot 3.2 (Nov 2023) added
@TestBean,@MockitoBean, and@MockitoSpyBeanas Mockito-native replacements for@MockBean/@SpyBean. The older annotations still work but the new ones are preferred for new code. - Spring Boot 3.3 (May 2024) extended Testcontainers support to Kafka, MongoDB, Redis, and other common backends out of the box, and improved restartable-context support for development tests.
- Spring Boot 3.4 (Nov 2024) further refined the
@MockitoBeanstory and brought stricter bean-override defaults: you must opt in to bean overriding withspring.main.allow-bean-definition-overriding=trueif you genuinely rely on it.
Pin which Boot version you’re on (./mvnw spring-boot:run --version or check spring-boot-starter-parent) before applying any fix below — the right import depends on it.
Fix 1: Choose the Right Test Annotation
Match the annotation to what you’re actually testing:
// @SpringBootTest — full application context
// Use for: integration tests that need the full stack
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTest {
@Autowired
private TestRestTemplate restTemplate; // Makes real HTTP calls
@Test
void testFullFlow() {
ResponseEntity<User> response = restTemplate.getForEntity("/api/users/1", User.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
// @WebMvcTest — web layer only (controllers, filters, @ControllerAdvice)
// Use for: testing controllers without starting the server or loading service/repository beans
@WebMvcTest(UserController.class) // Load only UserController
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // Must mock all dependencies that UserController injects
private UserService userService;
@Test
void testGetUser() throws Exception {
when(userService.findById("1")).thenReturn(new User("1", "Alice"));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
}
}
// @DataJpaTest — JPA/database layer only
// Use for: testing repositories with an in-memory database
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Test
void testFindByEmail() {
userRepository.save(new User(null, "Alice", "[email protected]"));
Optional<User> found = userRepository.findByEmail("[email protected]");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
}
// @Service / plain Mockito — pure unit test, no Spring context at all
// Use for: testing service logic in isolation — fastest option
class UserServiceTest {
@Mock
private UserRepository userRepository; // Mockito mock, not Spring
@InjectMocks
private UserService userService; // Injects mocks directly
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
void testCreateUser() {
when(userRepository.save(any())).thenReturn(new User("1", "Alice", "[email protected]"));
User created = userService.createUser("Alice", "[email protected]");
assertThat(created.getId()).isEqualTo("1");
}
}Fix 2: Set Up MockMvc Correctly
MockMvc can be configured in two ways — through @WebMvcTest (recommended) or manually:
// Method 1: @WebMvcTest — Spring creates MockMvc automatically
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private JwtTokenProvider jwtTokenProvider; // Mock all dependencies
@Autowired
private ObjectMapper objectMapper; // For JSON serialization
@Test
void testCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest("Alice", "[email protected]");
User created = new User("1", "Alice", "[email protected]");
when(userService.createUser(any(CreateUserRequest.class))).thenReturn(created);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value("1"))
.andExpect(jsonPath("$.name").value("Alice"))
.andDo(print()); // Prints request/response to console — useful for debugging
}
@Test
void testGetUserNotFound() throws Exception {
when(userService.findById("999")).thenThrow(new UserNotFoundException("999"));
mockMvc.perform(get("/api/users/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("User 999 not found"));
}
}
// Method 2: MockMvcBuilders.standaloneSetup — no Spring context at all
// Use when you don't need Spring's filter chain or exception handlers
class UserControllerStandaloneTest {
private MockMvc mockMvc;
@Mock
private UserService userService;
@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this);
mockMvc = MockMvcBuilders
.standaloneSetup(new UserController(userService))
.setControllerAdvice(new GlobalExceptionHandler()) // Add manually
.build();
}
}Fix 3: Fix @MockBean vs @Mock Confusion
Use @MockBean when the mock needs to be in the Spring context, @Mock for pure unit tests. On Spring Boot 3.4+, prefer @MockitoBean from org.springframework.test.context.bean.override.mockito — @MockBean from the boot package is now considered legacy.
// @WebMvcTest — use @MockBean (Spring manages injection)
@WebMvcTest(UserController.class)
class ControllerTest {
@MockBean
private UserService userService; // Replaces UserService bean in context
// Spring injects this into UserController automatically
}
// Boot 3.4+ — preferred modern form
@WebMvcTest(UserController.class)
class ControllerTest {
@MockitoBean
private UserService userService; // Same behaviour, framework-native
}
// Plain unit test — use @Mock + @InjectMocks (Mockito manages injection)
@ExtendWith(MockitoExtension.class) // Use this instead of MockitoAnnotations.openMocks()
class ServiceTest {
@Mock
private UserRepository userRepository; // Mockito mock
@InjectMocks
private UserService userService; // Mockito injects @Mock fields into this
@Test
void testFindUser() {
when(userRepository.findById("1")).thenReturn(Optional.of(new User("1", "Alice")));
User user = userService.findById("1");
assertThat(user.getName()).isEqualTo("Alice");
verify(userRepository, times(1)).findById("1");
}
}Spy on real beans:
@WebMvcTest(UserController.class)
class ControllerTest {
@SpyBean // Uses the real bean but allows stubbing specific methods
private UserService userService;
@Test
void testWithSpy() {
// Only stub the method you want to intercept
doReturn(new User("1", "Alice")).when(userService).findById("1");
// All other methods use the real implementation
}
}Fix 4: Configure Test Properties
Override application properties for tests without polluting the main config:
// Method 1: @TestPropertySource — override specific properties
@SpringBootTest
@TestPropertySource(properties = {
"spring.datasource.url=jdbc:h2:mem:testdb",
"app.feature.enabled=false",
"spring.mail.host=localhost"
})
class ApplicationTest { }
// Method 2: application-test.yml in src/test/resources
// Activated with @ActiveProfiles("test")
@SpringBootTest
@ActiveProfiles("test")
class ProfileTest { }
// src/test/resources/application-test.yml:
// spring:
// datasource:
// url: jdbc:h2:mem:testdb
// jpa:
// show-sql: true# src/test/resources/application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
mail:
host: localhost
port: 3025 # FakeSMTP or GreenMail
app:
jwt:
secret: test-secret-key-for-testing-only
feature-flags:
new-ui: falseUse Testcontainers — Boot 3.1+ with @ServiceConnection is the modern path:
// Boot 3.1+ — no @DynamicPropertySource needed
@SpringBootTest
@Testcontainers
class ModernContainerTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Autowired
private UserRepository userRepository;
@Test
void testWithRealDatabase() {
userRepository.save(new User(null, "Alice", "[email protected]"));
assertThat(userRepository.count()).isEqualTo(1);
}
}
// Pre-Boot-3.1 — explicit @DynamicPropertySource is still required
@SpringBootTest
@Testcontainers
class LegacyContainerTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}Fix 5: Test Security Configuration
Testing secured endpoints requires configuring the security context. On Boot 3 the import is org.springframework.security.test.context.support.WithMockUser (unchanged) but spring-security-test must be on the classpath — the spring-boot-starter-test does not pull it in transitively.
@WebMvcTest(UserController.class)
@Import(SecurityConfig.class) // Import security config if needed
class SecuredControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@MockBean
private UserDetailsService userDetailsService;
@MockBean
private JwtAuthenticationFilter jwtFilter;
// Test with @WithMockUser — simulates authenticated user
@Test
@WithMockUser(username = "alice", roles = {"USER"})
void testGetProfile() throws Exception {
when(userService.findByUsername("alice")).thenReturn(new User("alice"));
mockMvc.perform(get("/api/profile"))
.andExpect(status().isOk());
}
// Test with @WithMockUser with admin role
@Test
@WithMockUser(roles = {"ADMIN"})
void testAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}
// Test unauthorized access
@Test
void testUnauthenticated() throws Exception {
mockMvc.perform(get("/api/profile"))
.andExpect(status().isUnauthorized());
}
// Test with JWT token in header
@Test
void testWithJwtToken() throws Exception {
String token = generateTestToken("alice", List.of("ROLE_USER"));
mockMvc.perform(get("/api/profile")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}Fix 6: Speed Up Tests with Context Caching
Spring caches test contexts — avoid breaking the cache unnecessarily:
// BAD — each class with different @MockBean creates a new context
@WebMvcTest(UserController.class)
class UserControllerTest {
@MockBean UserService userService; // Context A
}
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@MockBean OrderService orderService; // Context B — different from A
@MockBean UserService userService; // Adding this means different context
}
// BETTER — share context by using the same set of mocks
// Create a base test class with all common mocks
@WebMvcTest
@Import({UserController.class, OrderController.class})
abstract class BaseControllerTest {
@MockBean UserService userService;
@MockBean OrderService orderService;
// Subclasses share this context
}
class UserControllerTest extends BaseControllerTest { }
class OrderControllerTest extends BaseControllerTest { }Use @DirtiesContext sparingly:
// @DirtiesContext forces a new context — use only when necessary
@DirtiesContext // Marks context as dirty after this test class
class DatabaseModifyingTest {
// Only use if you've genuinely modified the context
}
// Better alternative: use transactions to roll back after each test
@DataJpaTest
@Transactional // Each test runs in a transaction, rolled back after
class RepositoryTest {
@Autowired UserRepository userRepository;
@Test
void testSave() {
userRepository.save(new User(null, "Alice"));
// Transaction rolls back after test — no cleanup needed
}
}Still Not Working?
Context fails to load due to missing beans — @WebMvcTest only loads the web layer. If your controller injects a service that injects a repository that needs a DataSource, and you only have @WebMvcTest, Spring can’t find those beans. Add @MockBean for every dependency the controller needs, or use @SpringBootTest for integration tests.
Tests pass individually but fail together — shared static state, Mockito mocks not being reset between tests, or database state leaking between tests. Add @Transactional on test classes to auto-rollback, or explicitly call Mockito.reset(mock) in @AfterEach. For @WebMvcTest, Mockito mocks created with @MockBean are automatically reset after each test.
@Value fields are null in tests — when using standaloneSetup without Spring context, @Value fields aren’t injected. Either use ReflectionTestUtils.setField(controller, "fieldName", value) or use @WebMvcTest which loads the Spring context.
@Async methods execute synchronously in tests — Spring’s @Async requires a proxy, which only exists in the full context. In unit tests, async methods are called directly. If you need to test async behavior, use @SpringBootTest with the full context and wait for completion with Awaitility.
javax.persistence cannot be resolved after upgrading to Boot 3 — Spring Boot 3 / Spring 6 switched the entire stack to the jakarta.* namespace. Replace javax.persistence.Entity with jakarta.persistence.Entity, javax.servlet.* with jakarta.servlet.*, and rerun. IDE bulk-rename is the fastest fix; the Spring Boot Migrator tool also handles it.
@MockBean deprecation warning on Boot 3.4+ — Boot 3.4 marked the boot-internal @MockBean as legacy in favour of @MockitoBean from org.springframework.test.context.bean.override.mockito. The behaviour is identical for typical cases; switching the import silences the warning and aligns with the future direction.
Bean override exception: BeanDefinitionOverrideException — Boot 2.1+ disabled bean override by default. If your tests intentionally redefine a bean (rare but legitimate), set spring.main.allow-bean-definition-overriding=true in application-test.yml. Boot 3.4 tightened this further for @TestConfiguration — prefer @TestBean instead.
For related Spring issues, see Fix: Spring Boot Transaction Not Rolling Back, Fix: Spring Cache Not Working, Fix: Java Spring Bean Creation Exception, and Fix: Spring Data JPA Query Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Supertest Not Working — Requests Not Sending, Server Not Closing, or Assertions Failing
How to fix Supertest HTTP testing issues — Express and Fastify setup, async test patterns, authentication headers, file uploads, JSON body assertions, and Vitest/Jest integration.
Fix: Go Test Not Working — Tests Not Running, Failing Unexpectedly, or Coverage Not Collected
How to fix Go testing issues — test function naming, table-driven tests, t.Run subtests, httptest, testify assertions, and common go test flag errors.
Fix: Java Record Not Working — Compact Constructor Error, Serialization Fails, or Cannot Extend
How to fix Java record issues — compact constructor validation, custom accessor methods, Jackson serialization, inheritance restrictions, and when to use records vs regular classes.
Fix: OpenTelemetry Not Working — Traces Not Appearing, Spans Missing, or Exporter Connection Refused
How to fix OpenTelemetry issues — SDK initialization order, auto-instrumentation setup, OTLP exporter configuration, context propagation, and missing spans in Node.js, Python, and Java.