Skip to content

Fix: Spring Boot Test Not Working — ApplicationContext Fails to Load, MockMvc Returns 404, or @MockBean Not Injected

FixDevs · (Updated: )

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 database

Or tests pass individually but fail when run together:

Tests run: 15, Failures: 3, Errors: 0, Skipped: 0
[WARNING] Tests run in an unexpected order

Why 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.

  • @SpringBootTest loads 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@WebMvcTest loads only web layer beans; @DataJpaTest loads only JPA components. Using the wrong slice means your beans aren’t loaded, causing 404 or unsatisfied dependencies.
  • @MockBean replaces beans in the Spring context — it’s different from Mockito’s @Mock. @MockBean registers a mock in the Spring context; @Mock only creates a Mockito mock without Spring integration.
  • Context caching — Spring caches test contexts across tests. Modifying the context (e.g., using @DirtiesContext or @MockBean in 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 @SpringBootTest and the original suite of slice annotations (@WebMvcTest, @DataJpaTest, @JsonTest, @RestClientTest). Before 1.4 you had to wire @ContextConfiguration by 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 @RunWith no 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’s assertThat became the canonical style.
  • Spring Boot 2.4+ (Nov 2020 onward) stabilized the slice annotations and added @WebFluxTest improvements 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.standaloneSetup patterns 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.* (so javax.persistence becomes jakarta.persistence in @DataJpaTest entities), and removes Boot 1 / Boot 2 deprecations. If your test imports javax.servlet.http.HttpServletRequest, it will not compile on Boot 3.
  • Spring Boot 3.1 (May 2023) introduced first-class Testcontainers integration via @ServiceConnection and the spring.testcontainers.beans.startup property — @DynamicPropertySource is no longer required for the common databases. The ConnectionDetails API replaces hand-wired JDBC URLs.
  • Spring Boot 3.2 (Nov 2023) added @TestBean, @MockitoBean, and @MockitoSpyBean as 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 @MockitoBean story and brought stricter bean-override defaults: you must opt in to bean overriding with spring.main.allow-bean-definition-overriding=true if 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: false

Use 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles