Skip to content

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

FixDevs ·

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:

  • @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.

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:

// @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
}

// 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 @DynamicPropertySource for Testcontainers:

@SpringBootTest
@Testcontainers
class ContainerTest {
    @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);
    }

    @Autowired
    private UserRepository userRepository;

    @Test
    void testWithRealDatabase() {
        // Uses a real PostgreSQL container
        userRepository.save(new User(null, "Alice", "[email protected]"));
        assertThat(userRepository.count()).isEqualTo(1);
    }
}

Fix 5: Test Security Configuration

Testing secured endpoints requires configuring the security context:

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

For related Spring issues, see Fix: Spring Boot Transaction Not Rolling Back and Fix: Spring Cache 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