Skip to main content

Testing Annotations in Spring & JUnit

Who this guide is for

The Big Pictureโ€‹

When writing tests in a Spring Boot project, you deal with three layers of annotations:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Layer 1: JUnit 5 (Test Runner) โ”‚
โ”‚ @Test, @ParameterizedTest, @BeforeEach, @DisplayName โ”‚
โ”‚ โ†’ Controls WHAT runs, WHEN, and HOW โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Layer 2: Mockito (Test Doubles) โ”‚
โ”‚ @Mock, @Spy, @InjectMocks, @Captor โ”‚
โ”‚ โ†’ Controls DEPENDENCIES โ€” replace real objects with fakes โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ Layer 3: Spring Boot Test (Application Context) โ”‚
โ”‚ @SpringBootTest, @WebMvcTest, @DataJpaTest, @MockitoBean โ”‚
โ”‚ โ†’ Controls SPRING CONTEXT โ€” how much of the app loads โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Rule of Thumb
  • Unit tests โ†’ Layer 1 + Layer 2 only (no Spring context โ†’ fast โšก)
  • Integration tests โ†’ Layer 1 + Layer 3 (Spring context โ†’ slower ๐Ÿข, but tests real wiring)

Layer 1: JUnit 5 Core Annotationsโ€‹

Lifecycle Annotationsโ€‹

class OrderServiceTest {

@BeforeAll // Runs ONCE before all tests (must be static)
static void setupOnce() {
// Expensive one-time setup: start Testcontainers, load CSV, etc.
}

@BeforeEach // Runs BEFORE each test method
void setup() {
// Reset mocks, prepare test data, clean state
}

@Test
void shouldCalculateTotal() { /* test logic */ }

@AfterEach // Runs AFTER each test method
void tearDown() {
// Close resources, clear caches
}

@AfterAll // Runs ONCE after all tests (must be static)
static void cleanupOnce() {
// Stop containers, delete temp files
}
}
Execution order for 2 test methods:

@BeforeAll โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
โ”‚
โ”œโ”€โ”€ @BeforeEach โ†’ @Test (test1) โ†’ @AfterEach
โ”‚
โ”œโ”€โ”€ @BeforeEach โ†’ @Test (test2) โ†’ @AfterEach
โ”‚
@AfterAll โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

Test Annotationsโ€‹

AnnotationPurposeExample
@TestMarks a single test method@Test void shouldReturnUser()
@ParameterizedTestRuns the same test with different inputsSee example below
@RepeatedTest(5)Runs the test 5 times (flaky test detection)@RepeatedTest(5) void stressTest()
@DisplayNameHuman-readable test name in reports@DisplayName("Should reject expired coupons")
@DisabledSkips a test (with reason)@Disabled("Bug #1234 โ€” fix pending")
@Timeout(5)Fails if test takes longer than 5 seconds@Timeout(value = 5, unit = SECONDS)
@Tag("slow")Categorize tests for selective executionFilter in Maven: -Dgroups=slow
@NestedGroup related tests in inner classesOrganize by scenario

Parameterized Tests โ€” Test Many Inputsโ€‹

// โ”€โ”€ @ValueSource โ€” simple single-value inputs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@ParameterizedTest(name = "isPalindrome({0})")
@ValueSource(strings = {"racecar", "radar", "level", "madam"})
void shouldDetectPalindromes(String word) {
assertTrue(StringUtils.isPalindrome(word));
}

// โ”€โ”€ @CsvSource โ€” multiple arguments per test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@ParameterizedTest(name = "add({0}, {1}) = {2}")
@CsvSource({
"1, 1, 2",
"0, 0, 0",
"-1, 1, 0",
"100, 200, 300"
})
void shouldAddNumbers(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}

// โ”€โ”€ @MethodSource โ€” complex objects as test data โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@ParameterizedTest
@MethodSource("provideInvalidEmails")
void shouldRejectInvalidEmails(String email, String reason) {
ValidationResult result = validator.validate(email);
assertFalse(result.isValid(), "Expected invalid: " + reason);
}

static Stream<Arguments> provideInvalidEmails() {
return Stream.of(
Arguments.of("", "empty string"),
Arguments.of("no-at-sign", "missing @"),
Arguments.of("@no-local.com", "missing local part"),
Arguments.of("spaces [email protected]", "contains spaces"),
Arguments.of(null, "null value")
);
}

// โ”€โ”€ @EnumSource โ€” test all enum values โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@ParameterizedTest
@EnumSource(OrderStatus.class)
void allStatusesShouldHaveDisplayName(OrderStatus status) {
assertNotNull(status.getDisplayName());
assertFalse(status.getDisplayName().isBlank());
}

Nested Tests โ€” Organize by Scenarioโ€‹

@DisplayName("OrderService")
class OrderServiceTest {

@Nested
@DisplayName("when creating an order")
class WhenCreatingOrder {

@Test
@DisplayName("should save to database")
void shouldSave() { /* ... */ }

@Test
@DisplayName("should publish OrderCreated event")
void shouldPublishEvent() { /* ... */ }

@Nested
@DisplayName("with invalid input")
class WithInvalidInput {

@Test
@DisplayName("should throw on null product")
void shouldThrowOnNull() { /* ... */ }

@Test
@DisplayName("should throw on negative quantity")
void shouldThrowOnNegative() { /* ... */ }
}
}
}

Report output:

OrderService
โ”œโ”€โ”€ when creating an order
โ”‚ โ”œโ”€โ”€ โœ… should save to database
โ”‚ โ”œโ”€โ”€ โœ… should publish OrderCreated event
โ”‚ โ””โ”€โ”€ with invalid input
โ”‚ โ”œโ”€โ”€ โœ… should throw on null product
โ”‚ โ””โ”€โ”€ โœ… should throw on negative quantity

Layer 2: Mockito Annotationsโ€‹

AnnotationWhat It CreatesWhen To Use
@MockA mock (all methods return defaults)Replace a dependency you don't want to call
@SpyA spy wrapping a real objectNeed real behavior + override one method
@InjectMocksReal object with mocks injectedAuto-wire @Mock/@Spy into the class under test
@CaptorAn ArgumentCaptorCapture arguments passed to a mock for detailed assertions
Always use @ExtendWith(MockitoExtension.class)

Without this, @Mock, @Spy, and @InjectMocks do nothing โ€” they are not initialized. This is the #1 mistake beginners make.

Complete Example with @Captorโ€‹

@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {

@Mock private EmailGateway emailGateway;
@Mock private UserRepository userRepo;
@Captor private ArgumentCaptor<EmailMessage> emailCaptor;
@InjectMocks private NotificationService notificationService;

@Test
void sendOrderConfirmation_composesCorrectEmail() {
// Arrange
User user = new User("alice", "[email protected]");
Order order = new Order("ORD-001", new BigDecimal("99.99"));
when(userRepo.findById("alice")).thenReturn(Optional.of(user));

// Act
notificationService.sendOrderConfirmation("alice", order);

// Assert โ€” capture the actual email that was sent
verify(emailGateway).send(emailCaptor.capture());
EmailMessage sentEmail = emailCaptor.getValue();

assertEquals("[email protected]", sentEmail.getTo());
assertThat(sentEmail.getSubject()).contains("ORD-001");
assertThat(sentEmail.getBody()).contains("$99.99");
}

@Test
void sendOrderConfirmation_userNotFound_doesNotSendEmail() {
when(userRepo.findById("unknown")).thenReturn(Optional.empty());

assertThrows(UserNotFoundException.class,
() -> notificationService.sendOrderConfirmation("unknown", anyOrder()));

// Verify email was NEVER sent
verifyNoInteractions(emailGateway);
}
}

Common Mockito Methods Referenceโ€‹

// โ”€โ”€ Stubbing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
when(mock.method(arg)).thenReturn(value); // return value
when(mock.method(arg)).thenThrow(new RuntimeException()); // throw
when(mock.method(arg)).thenAnswer(invocation -> { // dynamic
String arg0 = invocation.getArgument(0);
return arg0.toUpperCase();
});

// โ”€โ”€ Verification โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
verify(mock).method(arg); // called exactly once
verify(mock, times(3)).method(arg); // called exactly 3 times
verify(mock, never()).method(arg); // never called
verify(mock, atLeastOnce()).method(any()); // at least once
verifyNoMoreInteractions(mock); // no other calls
verifyNoInteractions(mock); // zero calls total

// โ”€โ”€ Argument Matchers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
when(repo.findById(any())).thenReturn(...); // any argument
when(repo.findByName(eq("alice"))).thenReturn(...); // exact value
when(repo.findByAge(anyInt())).thenReturn(...);
when(repo.findByName(argThat(name -> name.startsWith("A")))).thenReturn(...);

Layer 3: Spring Boot Test Annotationsโ€‹

@SpringBootTest โ€” Full Application Contextโ€‹

// Loads the ENTIRE Spring context โ€” all beans, all config
// Use for end-to-end integration tests
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ApplicationIntegrationTest {

@Autowired
private TestRestTemplate restTemplate;

@Test
void healthCheck_returns200() {
ResponseEntity<String> response =
restTemplate.getForEntity("/actuator/health", String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
}
webEnvironment OptionBehavior
MOCK (default)Simulated servlet environment โ€” use MockMvc
RANDOM_PORTStarts real HTTP server on random port โ€” use TestRestTemplate
DEFINED_PORTStarts server on server.port
NONENo web environment at all โ€” service-layer tests

Sliced Test Annotations โ€” Load Only What You Needโ€‹

Full context (@SpringBootTest):
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Controllers + Services + Repos + Config + Security + โ”‚
โ”‚ Kafka + Redis + Scheduling + ... โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โฑ๏ธ Startup time: 5-30 seconds

@WebMvcTest slice:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Controllers + Filters + Advice โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โฑ๏ธ Startup time: 1-3 seconds

@DataJpaTest slice:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Repositories + JPA + In-mem DB โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โฑ๏ธ Startup time: 2-5 seconds

@WebMvcTest โ€” Controller Layer Onlyโ€‹

@WebMvcTest(OrderController.class) // only loads OrderController + web layer
class OrderControllerTest {

@Autowired private MockMvc mockMvc;

@MockitoBean // Spring Boot 3.4+
private OrderService orderService;

@Test
void getOrder_found_returns200() throws Exception {
when(orderService.findById("ORD-1"))
.thenReturn(Optional.of(new OrderDto("ORD-1", "SHIPPED")));

mockMvc.perform(get("/api/orders/ORD-1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orderId").value("ORD-1"))
.andExpect(jsonPath("$.status").value("SHIPPED"));
}

@Test
void getOrder_notFound_returns404() throws Exception {
when(orderService.findById("UNKNOWN"))
.thenReturn(Optional.empty());

mockMvc.perform(get("/api/orders/UNKNOWN"))
.andExpect(status().isNotFound());
}
}
Common @WebMvcTest Mistake

@WebMvcTest does NOT load @Service, @Repository, or @Component beans. If your controller depends on OrderService, you must provide a @MockitoBean for it. Forgetting this causes NoSuchBeanDefinitionException.

@DataJpaTest โ€” Repository Layer Onlyโ€‹

@DataJpaTest // auto-configures: JPA, in-memory H2, @Entity scanning
@AutoConfigureTestDatabase(replace = Replace.NONE) // use Testcontainers instead
class OrderRepositoryTest {

@Autowired private OrderRepository orderRepo;
@Autowired private TestEntityManager em;

@Test
void findByStatus_returnsMatchingOrders() {
em.persist(new Order("PROD-1", 2, "PENDING"));
em.persist(new Order("PROD-2", 1, "SHIPPED"));
em.persist(new Order("PROD-3", 3, "PENDING"));
em.flush();

List<Order> pending = orderRepo.findByStatus("PENDING");

assertEquals(2, pending.size());
assertTrue(pending.stream().allMatch(o -> o.getStatus().equals("PENDING")));
}

@Test
void customQuery_calculatesRevenueByCategory() {
// Test your @Query methods against real SQL
em.persist(new Order("laptop", 1, "COMPLETED", new BigDecimal("1299")));
em.persist(new Order("laptop", 1, "COMPLETED", new BigDecimal("999")));
em.flush();

BigDecimal revenue = orderRepo.calculateRevenueByCategory("laptop");
assertEquals(new BigDecimal("2298"), revenue);
}
}

@JsonTest โ€” Serialization/Deserialization Onlyโ€‹

@JsonTest
class OrderDtoJsonTest {

@Autowired private JacksonTester<OrderDto> json;

@Test
void serialize_producesExpectedJson() throws Exception {
OrderDto dto = new OrderDto("ORD-1", "SHIPPED", LocalDate.of(2024, 1, 15));

assertThat(json.write(dto))
.extractingJsonPathStringValue("$.orderId").isEqualTo("ORD-1")
.extractingJsonPathStringValue("$.status").isEqualTo("SHIPPED")
.extractingJsonPathStringValue("$.date").isEqualTo("2024-01-15");
}

@Test
void deserialize_parsesJsonCorrectly() throws Exception {
String content = """
{"orderId": "ORD-1", "status": "SHIPPED", "date": "2024-01-15"}
""";

assertThat(json.parse(content))
.isEqualTo(new OrderDto("ORD-1", "SHIPPED", LocalDate.of(2024, 1, 15)));
}
}

๐Ÿ“‹ Annotation Decision Matrixโ€‹

Use this table to pick the right annotation for your test scenario:

I want to test...AnnotationMockingContextSpeed
Pure business logic@ExtendWith(MockitoExtension.class)@Mock / @InjectMocksNoneโšก ~ms
Controller endpoints@WebMvcTest@MockitoBeanWeb slice๐Ÿข ~2s
JPA repository queries@DataJpaTestโ€”JPA slice๐Ÿข ~3s
JSON serialization@JsonTestโ€”JSON sliceโšก ~1s
Full app wiring@SpringBootTest@MockitoBean (optional)Full๐ŸŒ ~10s
External HTTP APIs@SpringBootTest + WireMockโ€”Full + mock server๐ŸŒ ~10s
Kafka consumer/producer@SpringBootTest + @EmbeddedKafkaโ€”Full + embedded๐ŸŒ ~15s

๐Ÿ”„ Spring Boot 3.4+ Migrationโ€‹

Spring Boot 3.4 introduced new annotations to replace the older @MockBean and @SpyBean:

Old (Deprecated)New (Spring Boot 3.4+)Change
@MockBean@MockitoBeanBetter context caching, clearer naming
@SpyBean@MockitoSpyBeanSame improvements
// โ”€โ”€ Before (Spring Boot < 3.4) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@SpringBootTest
class OldStyleTest {
@MockBean private PaymentClient paymentClient; // deprecated
@SpyBean private OrderService orderService; // deprecated
}

// โ”€โ”€ After (Spring Boot 3.4+) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@SpringBootTest
class NewStyleTest {
@MockitoBean private PaymentClient paymentClient; // โœ…
@MockitoSpyBean private OrderService orderService; // โœ…
}
Why The Change?

The old @MockBean sometimes caused context caching issues โ€” Spring would create separate application contexts for tests that use different @MockBean combinations, dramatically slowing down test suites. The new annotations are designed to integrate more cleanly with Spring's test context framework.


Interview Questionsโ€‹

For New Learnersโ€‹

Q: What is the difference between @Mock and @MockitoBean?

@Mock (Mockito) creates a mock in plain unit tests โ€” no Spring context is involved. @MockitoBean (Spring Boot) replaces an actual bean in the Spring ApplicationContext with a mock โ€” used in integration tests where Spring manages the wiring.

Q: Why use @WebMvcTest instead of @SpringBootTest for controller tests?

@WebMvcTest loads only the web layer (controllers, filters, advice) โ€” starting in ~2 seconds. @SpringBootTest loads the entire application context (services, repos, configs, Kafka, Redis...) โ€” starting in 10+ seconds. Use @WebMvcTest for speed when you only need to test HTTP request/response behavior.

Q: What does @BeforeEach do?

It marks a method that runs before every single @Test method in the class. Use it to reset mocks, prepare test data, or set up clean state so tests don't affect each other.

For Senior Engineersโ€‹

Q: How do you choose between @Mock, @MockitoBean, and @MockitoSpyBean?

Use @Mock in pure unit tests (no Spring context) โ€” fastest. Use @MockitoBean in integration tests when you need to replace a Spring-managed bean entirely (e.g., mock an external payment gateway). Use @MockitoSpyBean when you need the real bean behavior but want to verify or override one specific method (e.g., spy on a service to verify event publishing while keeping real business logic).

Q: Why can excessive @SpringBootTest usage slow teams down?

Each unique combination of @MockitoBean declarations creates a separate Spring context. If 50 test classes each mock different beans, Spring may create 50 separate contexts โ€” each taking 10+ seconds to start. Fix: standardize mock combinations using a shared base class, or prefer @WebMvcTest/@DataJpaTest slices.

Q: What signals indicate your test annotation strategy is wrong?

(1) Test suite takes 15+ minutes to run locally. (2) Tests fail when reordered. (3) Every test class uses @SpringBootTest even for pure logic. (4) You see ApplicationContext was cached X times warnings with high X values. (5) Developers skip running tests locally because they're too slow.

Q: How should you handle deprecated @MockBean during a Spring Boot upgrade?

Migrate incrementally: start with new test classes using @MockitoBean, then update existing tests component-by-component. Both annotations work simultaneously during the migration window. Add a checkstyle/lint rule to prevent new usages of @MockBean and track migration progress.

Q: How do you test configuration properties safely?

Use @SpringBootTest(classes = {MyConfig.class}) with minimal config scope instead of loading the full app. Test binding, defaults, validation (@Validated), and profile-specific overrides (@ActiveProfiles("test")). Use @ConfigurationPropertiesTest for focused property binding tests.