← Back to Advanced Java
Bonus

Testing with Mockito

Master unit testing with mocks, stubs, and behavior verification

Duration2-3 hours
DifficultyIntermediate
SessionBonus - Testing

What You Will Learn

Testing is essential for reliable software. In this practical work, you'll master:

  • Mocking - Creating fake objects that simulate real dependencies
  • Stubbing - Defining what mocks should return
  • Verification - Checking that methods were called correctly
  • ArgumentCaptor - Capturing arguments for inspection
  • @Spy - Partial mocking of real objects
  • BDD Style - Given/When/Then testing pattern

Understanding Mocking

What is Mocking?

Imagine testing a car's speedometer. You don't need to actually drive at 100 mph - you just need to simulate the wheel spinning fast. A mock is a fake object that simulates a real dependency.


Real Code:
┌─────────────────┐        ┌─────────────────┐
│  OrderService   │───────▶│  PaymentGateway │ (calls real bank API)
│                 │        │  (external API) │
└─────────────────┘        └─────────────────┘

Unit Test with Mock:
┌─────────────────┐        ┌─────────────────┐
│  OrderService   │───────▶│  Mock Gateway   │ (returns fake success)
│  (tested code)  │        │  (controlled)   │
└─────────────────┘        └─────────────────┘

Benefits:
- Fast tests (no network calls)
- Reliable tests (no external failures)
- Control test scenarios (success, failure, timeout)

Prerequisites

  • Java 17+ and Maven installed
  • Basic JUnit 5 knowledge
  • Understanding of dependency injection

Part 1: Project Setup

Step 1.1: Create Project Structure

mockito-lab/
├── pom.xml
└── src/
    ├── main/java/com/example/shop/
    │   ├── model/
    │   ├── repository/
    │   └── service/
    └── test/java/com/example/shop/
        └── service/

Step 1.2: Create pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>mockito-lab</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- (#1:JUnit 5) -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>

        <!-- (#2:Mockito Core) -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>

        <!-- (#3:Mockito JUnit 5 Extension) -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

Part 2: Create Business Logic

Step 2.1: Create Models

Create src/main/java/com/example/shop/model/Order.java:

package com.example.shop.model;

import java.math.BigDecimal;
import java.util.List;

public class Order {
    private Long id;
    private Long customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    private BigDecimal total;

    public Order(Long id, Long customerId, List<OrderItem> items) {
        this.id = id;
        this.customerId = customerId;
        this.items = items;
        this.status = OrderStatus.PENDING;
        this.total = items.stream()
                .map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    // Getters and Setters
    public Long getId() { return id; }
    public Long getCustomerId() { return customerId; }
    public List<OrderItem> getItems() { return items; }
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) { this.status = status; }
    public BigDecimal getTotal() { return total; }
}

Create src/main/java/com/example/shop/model/OrderItem.java:

package com.example.shop.model;

import java.math.BigDecimal;

public class OrderItem {
    private Long productId;
    private String productName;
    private int quantity;
    private BigDecimal price;

    public OrderItem(Long productId, String productName, int quantity, BigDecimal price) {
        this.productId = productId;
        this.productName = productName;
        this.quantity = quantity;
        this.price = price;
    }

    public Long getProductId() { return productId; }
    public String getProductName() { return productName; }
    public int getQuantity() { return quantity; }
    public BigDecimal getPrice() { return price; }
}

Create src/main/java/com/example/shop/model/OrderStatus.java:

package com.example.shop.model;

public enum OrderStatus {
    PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}

Step 2.2: Create Interfaces (Dependencies)

Create src/main/java/com/example/shop/repository/OrderRepository.java:

package com.example.shop.repository;

import com.example.shop.model.Order;
import java.util.Optional;

public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(Long id);
    void delete(Long id);
}

Create src/main/java/com/example/shop/service/PaymentGateway.java:

package com.example.shop.service;

import java.math.BigDecimal;

public interface PaymentGateway {
    boolean processPayment(Long customerId, BigDecimal amount);
    void refund(Long customerId, BigDecimal amount);
}

Create src/main/java/com/example/shop/service/NotificationService.java:

package com.example.shop.service;

public interface NotificationService {
    void sendOrderConfirmation(Long customerId, Long orderId);
    void sendPaymentFailure(Long customerId, String reason);
}

Step 2.3: Create OrderService (Class Under Test)

Create src/main/java/com/example/shop/service/OrderService.java:

package com.example.shop.service;

import com.example.shop.model.*;
import com.example.shop.repository.OrderRepository;

public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;
    private final NotificationService notificationService;

    // (#1:Dependencies injected via constructor)
    public OrderService(OrderRepository orderRepository,
                        PaymentGateway paymentGateway,
                        NotificationService notificationService) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
        this.notificationService = notificationService;
    }

    public Order placeOrder(Order order) {
        // (#2:Save order first)
        Order savedOrder = orderRepository.save(order);

        // (#3:Process payment)
        boolean paymentSuccess = paymentGateway.processPayment(
                order.getCustomerId(),
                order.getTotal()
        );

        if (paymentSuccess) {
            savedOrder.setStatus(OrderStatus.PAID);
            orderRepository.save(savedOrder);
            notificationService.sendOrderConfirmation(
                    order.getCustomerId(),
                    savedOrder.getId()
            );
        } else {
            savedOrder.setStatus(OrderStatus.CANCELLED);
            orderRepository.save(savedOrder);
            notificationService.sendPaymentFailure(
                    order.getCustomerId(),
                    "Payment declined"
            );
        }

        return savedOrder;
    }

    public void cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new IllegalArgumentException("Order not found"));

        if (order.getStatus() == OrderStatus.PAID) {
            paymentGateway.refund(order.getCustomerId(), order.getTotal());
        }

        order.setStatus(OrderStatus.CANCELLED);
        orderRepository.save(order);
    }
}

Part 3: Basic Mocking

Step 3.1: Create Your First Mock Test

Create src/test/java/com/example/shop/service/OrderServiceTest.java:

package com.example.shop.service;

import com.example.shop.model.*;
import com.example.shop.repository.OrderRepository;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)  // (#1:Enable Mockito annotations)
class OrderServiceTest {

    @Mock  // (#2:Create a mock of this interface)
    private OrderRepository orderRepository;

    @Mock
    private PaymentGateway paymentGateway;

    @Mock
    private NotificationService notificationService;

    @InjectMocks  // (#3:Inject mocks into this object)
    private OrderService orderService;

    private Order testOrder;

    @BeforeEach
    void setUp() {
        OrderItem item = new OrderItem(1L, "Laptop", 1, new BigDecimal("999.99"));
        testOrder = new Order(1L, 100L, List.of(item));
    }

    @Test
    @DisplayName("Place order successfully when payment succeeds")
    void placeOrder_PaymentSuccess() {
        // (#4:Arrange - Define mock behavior)
        when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
        when(paymentGateway.processPayment(eq(100L), any(BigDecimal.class))).thenReturn(true);

        // (#5:Act - Call the method under test)
        Order result = orderService.placeOrder(testOrder);

        // (#6:Assert - Verify the result)
        assertEquals(OrderStatus.PAID, result.getStatus());

        // (#7:Verify - Check mock interactions)
        verify(orderRepository, times(2)).save(any(Order.class));  // Saved twice
        verify(paymentGateway).processPayment(eq(100L), any(BigDecimal.class));
        verify(notificationService).sendOrderConfirmation(100L, 1L);
        verify(notificationService, never()).sendPaymentFailure(anyLong(), anyString());
    }

    @Test
    @DisplayName("Cancel order when payment fails")
    void placeOrder_PaymentFails() {
        // Arrange
        when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
        when(paymentGateway.processPayment(anyLong(), any(BigDecimal.class))).thenReturn(false);

        // Act
        Order result = orderService.placeOrder(testOrder);

        // Assert
        assertEquals(OrderStatus.CANCELLED, result.getStatus());

        // Verify correct notifications sent
        verify(notificationService, never()).sendOrderConfirmation(anyLong(), anyLong());
        verify(notificationService).sendPaymentFailure(eq(100L), eq("Payment declined"));
    }
}

Step 3.2: Run the Tests

cd mockito-lab
mvn test

Expected output:

[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Part 4: Common Stubbing Patterns

Step 4.1: Different Ways to Stub

Add these tests to understand stubbing options:

@Test
@DisplayName("Stubbing patterns demonstration")
void stubbingPatterns() {
    // (#1:Return specific value)
    when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);

    // (#2:Return different values on consecutive calls)
    when(paymentGateway.processPayment(100L, new BigDecimal("50.00")))
            .thenReturn(true)   // First call
            .thenReturn(false)  // Second call
            .thenReturn(true);  // Third and subsequent calls

    // (#3:Throw an exception)
    when(orderRepository.findById(999L))
            .thenThrow(new RuntimeException("Database error"));

    // (#4:Answer - dynamic response based on arguments)
    when(orderRepository.save(any(Order.class))).thenAnswer(invocation -> {
        Order order = invocation.getArgument(0);
        // Return the same order (simulate save)
        return order;
    });

    // (#5:Do nothing for void methods)
    doNothing().when(notificationService).sendOrderConfirmation(anyLong(), anyLong());

    // (#6:Throw exception for void methods)
    doThrow(new RuntimeException("Email server down"))
            .when(notificationService).sendPaymentFailure(anyLong(), anyString());
}

Step 4.2: Argument Matchers

@Test
@DisplayName("Argument matchers demonstration")
void argumentMatchers() {
    // (#1:any() - match any value)
    when(orderRepository.save(any(Order.class))).thenReturn(testOrder);

    // (#2:eq() - exact match (use with other matchers))
    when(paymentGateway.processPayment(eq(100L), any(BigDecimal.class))).thenReturn(true);

    // (#3:anyLong(), anyString(), etc.)
    when(orderRepository.findById(anyLong())).thenReturn(java.util.Optional.of(testOrder));

    // (#4:Custom matcher with argThat)
    when(paymentGateway.processPayment(
            anyLong(),
            argThat(amount -> amount.compareTo(BigDecimal.ZERO) > 0)))  // Amount must be positive
            .thenReturn(true);

    // (#5:isNull(), isNotNull()
    when(orderRepository.findById(isNull())).thenReturn(java.util.Optional.empty());
}
Important Rule!

If you use an argument matcher for one argument, you must use matchers for ALL arguments:

// WRONG - mixing matcher and raw value
when(mock.method(anyLong(), 100L)).thenReturn(result);

// CORRECT - use eq() for raw values
when(mock.method(anyLong(), eq(100L))).thenReturn(result);

Part 5: Verification Patterns

Step 5.1: Verify Method Calls

@Test
@DisplayName("Verification patterns demonstration")
void verificationPatterns() {
    // Setup
    when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
    when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);

    // Execute
    orderService.placeOrder(testOrder);

    // (#1:Verify method was called)
    verify(paymentGateway).processPayment(anyLong(), any());

    // (#2:Verify exact number of calls)
    verify(orderRepository, times(2)).save(any(Order.class));

    // (#3:Verify at least/at most)
    verify(orderRepository, atLeast(1)).save(any());
    verify(orderRepository, atMost(3)).save(any());

    // (#4:Verify never called)
    verify(notificationService, never()).sendPaymentFailure(anyLong(), anyString());

    // (#5:Verify call order)
    InOrder inOrder = inOrder(orderRepository, paymentGateway, notificationService);
    inOrder.verify(orderRepository).save(any());  // First
    inOrder.verify(paymentGateway).processPayment(anyLong(), any());  // Then
    inOrder.verify(notificationService).sendOrderConfirmation(anyLong(), anyLong());  // Last

    // (#6:Verify no more interactions)
    verifyNoMoreInteractions(paymentGateway);
}

Part 6: ArgumentCaptor

Step 6.1: Capture and Inspect Arguments

@Test
@DisplayName("Use ArgumentCaptor to inspect method arguments")
void argumentCaptor() {
    // Setup
    when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
    when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);

    // Execute
    orderService.placeOrder(testOrder);

    // (#1:Create captor)
    ArgumentCaptor<Order> orderCaptor = ArgumentCaptor.forClass(Order.class);

    // (#2:Capture the argument)
    verify(orderRepository, times(2)).save(orderCaptor.capture());

    // (#3:Get all captured values)
    List<Order> capturedOrders = orderCaptor.getAllValues();
    assertEquals(2, capturedOrders.size());

    // (#4:Inspect first call (initial save)
    Order firstSave = capturedOrders.get(0);
    assertEquals(OrderStatus.PENDING, firstSave.getStatus());

    // (#5:Inspect second call (after payment)
    Order secondSave = capturedOrders.get(1);
    assertEquals(OrderStatus.PAID, secondSave.getStatus());
}

@Test
@DisplayName("Capture primitive arguments")
void captorForPrimitives() {
    when(orderRepository.save(any(Order.class))).thenReturn(testOrder);
    when(paymentGateway.processPayment(anyLong(), any())).thenReturn(true);

    orderService.placeOrder(testOrder);

    // (#6:Capture BigDecimal argument)
    ArgumentCaptor<BigDecimal> amountCaptor = ArgumentCaptor.forClass(BigDecimal.class);
    verify(paymentGateway).processPayment(anyLong(), amountCaptor.capture());

    BigDecimal capturedAmount = amountCaptor.getValue();
    assertEquals(new BigDecimal("999.99"), capturedAmount);
}
When to use ArgumentCaptor?
  • When you need to inspect complex objects passed to mocks
  • When simple matchers aren't enough
  • When you want to verify multiple calls with different arguments

Part 7: @Spy - Partial Mocking

Step 7.1: Understanding @Spy

Create src/main/java/com/example/shop/service/DiscountCalculator.java:

package com.example.shop.service;

import java.math.BigDecimal;
import java.math.RoundingMode;

public class DiscountCalculator {

    public BigDecimal calculateDiscount(BigDecimal total, int loyaltyPoints) {
        BigDecimal baseDiscount = calculateBaseDiscount(total);
        BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(loyaltyPoints);
        return baseDiscount.add(loyaltyDiscount);
    }

    // (#1:We might want to mock this independently)
    public BigDecimal calculateBaseDiscount(BigDecimal total) {
        if (total.compareTo(new BigDecimal("100")) >= 0) {
            return total.multiply(new BigDecimal("0.10")).setScale(2, RoundingMode.HALF_UP);
        }
        return BigDecimal.ZERO;
    }

    public BigDecimal calculateLoyaltyDiscount(int points) {
        return new BigDecimal(points).multiply(new BigDecimal("0.01")).setScale(2, RoundingMode.HALF_UP);
    }
}

Step 7.2: Using @Spy

Create src/test/java/com/example/shop/service/DiscountCalculatorTest.java:

package com.example.shop.service;

import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class DiscountCalculatorTest {

    @Spy  // (#1:Real object with selective mocking)
    private DiscountCalculator calculator;

    @Test
    @DisplayName("@Spy allows partial mocking")
    void partialMocking() {
        // (#2:Override one method, keep others real)
        doReturn(new BigDecimal("20.00"))
                .when(calculator).calculateBaseDiscount(any(BigDecimal.class));

        // (#3:Call the main method)
        BigDecimal result = calculator.calculateDiscount(
                new BigDecimal("150.00"),
                50  // 50 points = $0.50 loyalty discount
        );

        // (#4:Base discount is mocked (20.00), loyalty is real (0.50))
        assertEquals(new BigDecimal("20.50"), result);

        // (#5:Verify the mocked method was called)
        verify(calculator).calculateBaseDiscount(new BigDecimal("150.00"));
    }

    @Test
    @DisplayName("@Spy uses real methods by default")
    void realMethodsByDefault() {
        // No stubbing - uses real implementation
        BigDecimal result = calculator.calculateDiscount(
                new BigDecimal("200.00"),  // 10% = $20
                100                         // 100 points = $1.00
        );

        assertEquals(new BigDecimal("21.00"), result);
    }
}
@Mock vs @Spy
@Mock@Spy
All methods return default (null, 0, false)All methods use real implementation
Must stub every method you callOnly stub methods you want to override
Use for interfaces/dependenciesUse for partial mocking of concrete classes

Part 8: BDD Style Testing

Step 8.1: Given-When-Then Pattern

package com.example.shop.service;

import com.example.shop.model.*;
import com.example.shop.repository.OrderRepository;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;

// (#1:Import BDD style methods)
import static org.mockito.BDDMockito.*;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(MockitoExtension.class)
class OrderServiceBDDTest {

    @Mock private OrderRepository orderRepository;
    @Mock private PaymentGateway paymentGateway;
    @Mock private NotificationService notificationService;
    @InjectMocks private OrderService orderService;

    @Test
    @DisplayName("Should send confirmation when payment succeeds")
    void shouldSendConfirmationWhenPaymentSucceeds() {
        // (#2:GIVEN - setup preconditions)
        OrderItem item = new OrderItem(1L, "Book", 2, new BigDecimal("25.00"));
        Order order = new Order(1L, 100L, List.of(item));

        given(orderRepository.save(any(Order.class))).willReturn(order);
        given(paymentGateway.processPayment(eq(100L), any())).willReturn(true);

        // (#3:WHEN - execute the action)
        Order result = orderService.placeOrder(order);

        // (#4:THEN - verify outcomes)
        then(orderRepository).should(times(2)).save(any(Order.class));
        then(paymentGateway).should().processPayment(eq(100L), eq(new BigDecimal("50.00")));
        then(notificationService).should().sendOrderConfirmation(100L, 1L);
        then(notificationService).shouldHaveNoMoreInteractions();

        assertEquals(OrderStatus.PAID, result.getStatus());
    }

    @Test
    @DisplayName("Should send failure notification when payment fails")
    void shouldSendFailureNotificationWhenPaymentFails() {
        // GIVEN
        OrderItem item = new OrderItem(1L, "Book", 1, new BigDecimal("10.00"));
        Order order = new Order(2L, 200L, List.of(item));

        given(orderRepository.save(any(Order.class))).willReturn(order);
        given(paymentGateway.processPayment(anyLong(), any())).willReturn(false);

        // WHEN
        Order result = orderService.placeOrder(order);

        // THEN
        then(notificationService).should(never()).sendOrderConfirmation(anyLong(), anyLong());
        then(notificationService).should().sendPaymentFailure(eq(200L), anyString());

        assertEquals(OrderStatus.CANCELLED, result.getStatus());
    }
}
BDD Style Benefits
  • given() is more readable than when() for setup
  • then().should() reads like a specification
  • Tests read like documentation
  • Follows the Arrange-Act-Assert pattern naturally

Deliverables Checklist

  • OrderServiceTest runs with all tests passing
  • Used @Mock and @InjectMocks correctly
  • Used when().thenReturn() for stubbing
  • Used verify() for interaction testing
  • Used ArgumentCaptor to inspect arguments
  • Used @Spy for partial mocking
  • Converted at least one test to BDD style

Mockito Cheat Sheet

Quick Reference
// Stubbing
when(mock.method()).thenReturn(value);
when(mock.method()).thenThrow(exception);
doNothing().when(mock).voidMethod();
doThrow(exception).when(mock).voidMethod();

// Verification
verify(mock).method();
verify(mock, times(2)).method();
verify(mock, never()).method();
verify(mock, atLeast(1)).method();
verifyNoMoreInteractions(mock);

// Matchers
any(), anyLong(), anyString()
eq(value)
argThat(arg -> condition)
isNull(), isNotNull()

// Captor
ArgumentCaptor<Type> captor = ArgumentCaptor.forClass(Type.class);
verify(mock).method(captor.capture());
Type captured = captor.getValue();
List<Type> allValues = captor.getAllValues();

// BDD
given(mock.method()).willReturn(value);
then(mock).should().method();
then(mock).should(never()).method();

Bonus Challenges

Challenge 1: Timeout Verification

Use verify(mock, timeout(1000)).method() to verify a method was called within a time limit.

Challenge 2: Mock Static Methods

Use mockStatic() to mock static methods like LocalDateTime.now().

Challenge 3: Integration with Spring

Use @MockBean in a Spring Boot test to replace beans with mocks.