Unit Testing

Write effective unit tests for Spring Boot services using JUnit 5, Mockito, and AssertJ — fast, isolated tests that don't start a Spring context.

Last reviewed: January 2025

·Spring Boot 2.7Spring Boot 3.xJUnit 5Mockito 5
✅
Quick Understanding: Unit tests test one class in isolation. For Spring Boot services, use Mockito to mock dependencies and inject them via constructor. No @SpringBootTest — that starts the full context and is slow. A unit test should run in milliseconds. Use @ExtendWith(MockitoExtension.class), @Mock for dependencies, @InjectMocks for the class under test.

🔍 What Is It?

Unit testing in Spring Boot means testing individual classes in isolation — no Spring context, no database, no HTTP.

Key libraries (all in spring-boot-starter-test):

  • →JUnit 5 — test framework (@Test, @BeforeEach, @ParameterizedTest)
  • →Mockito — mock framework (@Mock, @InjectMocks, when/verify)
  • →AssertJ — fluent assertions (assertThat(result).isEqualTo(expected))

Unit test characteristics:

  • →Tests one class (the "unit")
  • →All dependencies are mocked
  • →No Spring context, no database
  • →Runs in < 100ms
  • →Tests behavior, not implementation

🎯 Why Do We Need It?

Unit tests are the foundation of a test suite:

  • →Fast — run in milliseconds, not seconds
  • →Isolated — failures point directly to the broken class
  • →Cheap — no infrastructure needed
  • →Design feedback — hard-to-test code indicates design problems

Constructor injection makes Spring services naturally unit-testable — just pass mocks to the constructor.

🏗️ What Happens in Real Projects?

Service layer unit tests are the most valuable:

  • →Test business logic with various inputs
  • →Test error conditions and exception throwing
  • →Test interactions with repositories (verify correct methods called)
  • →Test edge cases that are hard to reproduce with real data

⚙️ How It Works

Mockito creates proxy objects that implement interfaces or extend classes. When you call a method on a mock, it returns null/0/false by default. Use when(mock.method()).thenReturn(value) to configure behavior. Use verify(mock).method() to assert the method was called.

💻 Example

Unit Testing Spring Services — Testing service logic with Mockito mocks.

java
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private PaymentService paymentService;

    @Mock
    private InventoryService inventoryService;

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void placeOrder_success_returnsCreatedOrder() {
        // Given
        CreateOrderRequest request = new CreateOrderRequest(
            1L, List.of(new OrderItemRequest(10L, 2)), "123 Main St");
        Order savedOrder = Order.builder()
            .id(42L)
            .orderNumber("ORD-001")
            .status(OrderStatus.PENDING)
            .build();

        when(orderRepository.save(any(Order.class))).thenReturn(savedOrder);

        // When
        OrderDetailDto result = orderService.placeOrder(request);

        // Then
        assertThat(result).isNotNull();
        assertThat(result.getId()).isEqualTo(42L);
        assertThat(result.getOrderNumber()).isEqualTo("ORD-001");

        verify(inventoryService).reserve(request.getItems());
        verify(paymentService).charge(savedOrder);
        verify(notificationService).sendConfirmation(savedOrder);
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    void placeOrder_paymentFails_rollsBack() {
        // Given
        CreateOrderRequest request = new CreateOrderRequest(1L, List.of(), "123 Main St");
        Order savedOrder = Order.builder().id(1L).build();

        when(orderRepository.save(any())).thenReturn(savedOrder);
        doThrow(new PaymentException("Card declined"))
            .when(paymentService).charge(savedOrder);

        // When + Then
        assertThatThrownBy(() -> orderService.placeOrder(request))
            .isInstanceOf(PaymentException.class)
            .hasMessage("Card declined");

        // Notification should NOT be sent if payment failed
        verify(notificationService, never()).sendConfirmation(any());
    }

    @Test
    void getOrder_notFound_throwsException() {
        // Given
        when(orderRepository.findById(99L)).thenReturn(Optional.empty());

        // When + Then
        assertThatThrownBy(() -> orderService.findById(99L, "user@example.com"))
            .isInstanceOf(ResourceNotFoundException.class)
            .hasMessageContaining("99");
    }

    // Parameterized test for multiple inputs
    @ParameterizedTest
    @ValueSource(strings = {"CANCELLED", "DELIVERED", "REFUNDED"})
    void cancelOrder_invalidStatus_throwsException(String status) {
        Order order = Order.builder()
            .id(1L)
            .status(OrderStatus.valueOf(status))
            .build();
        when(orderRepository.findById(1L)).thenReturn(Optional.of(order));

        assertThatThrownBy(() -> orderService.cancelOrder(1L, new CancelOrderRequest(), "user"))
            .isInstanceOf(BusinessException.class);
    }
}

🤔 Common Confusions

❓ @InjectMocks vs @Autowired — which to use in tests?

@InjectMocks is Mockito's annotation — it creates the class under test and injects @Mock fields into it. Use it for unit tests (no Spring context). @Autowired is Spring's annotation — use it in integration tests where Spring creates and wires beans. Never use @Autowired in pure unit tests.

❓ When should I use @Mock vs @MockBean?

@Mock is Mockito — creates a mock without Spring. Use in unit tests with @ExtendWith(MockitoExtension.class). @MockBean is Spring Boot — creates a mock AND registers it in the Spring ApplicationContext. Use in integration tests with @SpringBootTest or @WebMvcTest. @MockBean is much slower because it requires a Spring context.

🚨 Common Production Issues

Tests using @SpringBootTest when they should be unit tests — 10x slower startup for no benefit

Tests not isolated — shared state between tests causing flaky failures

Mocking too much — if you mock everything, you're not testing anything

Not testing error cases — happy path only leads to production surprises

✅ Best Practices

Use @ExtendWith(MockitoExtension.class) for unit tests — no Spring context

Follow Given-When-Then structure for readable tests

Test one behavior per test method

Test error cases as thoroughly as happy paths

Use @ParameterizedTest for testing multiple inputs

Aim for 80%+ coverage on the service layer

🏛️ Architect's Note

Senior Architect's Perspective

The testing pyramid: many unit tests (fast, cheap), fewer integration tests (slower, more realistic), few E2E tests (slowest, most realistic). Unit tests give you fast feedback during development. If your unit tests are slow, you're probably starting a Spring context — that's an integration test, not a unit test. Keep unit tests pure: no Spring, no database, no network.

💬 Frequently Asked Questions

How do I test a method that calls LocalDateTime.now()?

Inject a Clock bean and use Clock.fixed() in tests. Or use @MockBean to mock the service that uses time. Avoid static calls to LocalDateTime.now() in business logic — they're untestable. Pass a Clock to the class via constructor injection.

How do I test void methods?

Use verify() to assert interactions: verify(mockRepository).save(expectedEntity). Use doThrow() to test exception handling: doThrow(new RuntimeException()).when(mock).method(). Use ArgumentCaptor to capture and assert what was passed to the mock.

🎯 Key Takeaways

1@ExtendWith(MockitoExtension.class) + @Mock + @InjectMocks for unit tests
2No Spring context in unit tests — they should run in milliseconds
3Given-When-Then structure makes tests readable
4Test both success and failure cases
5@Mock is Mockito (no Spring); @MockBean is Spring Boot (requires context)
6Constructor injection makes services naturally unit-testable

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Spring Boot references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide Spring Boot learning path

Browse the full Spring Boot topic collection and learning order on SplashRide.

Spring Boot Interview Prep

Practice Spring Boot interview questions and scenario-based answers.

Backend Engineer Career Path

A structured journey through Core Java, Spring Boot, production backend engineering, and interview preparation.

Full Stack Java Developer Career Path

A guided route through frontend foundations, Java backend engineering, delivery workflows, and interview readiness.

Integration Testing

Write Spring Boot integration tests with @SpringBootTest, Testcontainers, and @DataJpaTest — testing real component interactions with actual databases.

MockMvc

Test Spring MVC controllers with MockMvc — the fluent API for testing HTTP request handling, response status, JSON content, and security without starting a real server.