Module 3: Mocking Review: TDD with Mockito

Module Overview

Review mocking techniques and learn how to apply them in the context of test-driven development using Mockito.

Learning Objectives

  • Understand why mocking is essential for effective unit testing
  • Review how to isolate components using mock objects
  • Master the Mockito framework for creating mocks and stubs
    • Creating mock objects with Mockito.mock()
    • Stubbing methods with when() and thenReturn()
    • Verifying method calls with verify()
    • Using argument matchers like any(), eq()
  • Apply TDD principles while utilizing mocks
    • Writing tests first with mocked dependencies
    • Implementing against the test specifications
    • Refactoring while maintaining test coverage
  • Mock different types of dependencies appropriately
    • External services and APIs
    • Databases and repositories
    • Complex business logic components
  • Use annotations to simplify mock creation
    • @Mock for creating mock objects
    • @InjectMocks for injecting mocks into tested objects
    • @Spy for partial mocking
  • Test exception handling using mocks

Unit Testing with Mocks

With Mockito, a mocking framework, we can write unit tests that utilize one type of test double called a mock.

Here is an excerpt from Martin Fowler's blog about this concept called test doubles:

  • Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
  • Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an InMemoryTestDatabase is a good example).
  • Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
  • Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
  • Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.

We use mocks to test dependencies without the cost of that dependency! We don't actually want to make a call to AWS or, by golly, implement AWS... We want to mock it instead!

In this short lesson, you will learn how to mock a class, when to do so, and how to use mocks with dependency injection.

Code Example: Mocking with Mockito

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

// Example service to be tested
public class OrderService {
    private final PaymentGateway paymentGateway;
    private final OrderRepository orderRepository;
    
    public OrderService(PaymentGateway paymentGateway, OrderRepository orderRepository) {
        this.paymentGateway = paymentGateway;
        this.orderRepository = orderRepository;
    }
    
    public OrderResult placeOrder(Order order) {
        // Process payment
        PaymentResult paymentResult = paymentGateway.processPayment(order.getAmount());
        
        if (paymentResult.isSuccessful()) {
            // Save order if payment successful
            orderRepository.save(order);
            return new OrderResult(true, "Order placed successfully");
        } else {
            return new OrderResult(false, "Payment failed: " + paymentResult.getMessage());
        }
    }
}

// Test using Mockito
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock
    private PaymentGateway paymentGateway;
    
    @Mock
    private OrderRepository orderRepository;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    void shouldPlaceOrderWhenPaymentSuccessful() {
        // Arrange
        Order order = new Order("123", 100.0);
        PaymentResult paymentResult = new PaymentResult(true, "Payment successful");
        
        when(paymentGateway.processPayment(100.0)).thenReturn(paymentResult);
        
        // Act
        OrderResult result = orderService.placeOrder(order);
        
        // Assert
        assertTrue(result.isSuccessful());
        assertEquals("Order placed successfully", result.getMessage());
        
        // Verify interactions
        verify(paymentGateway).processPayment(100.0);
        verify(orderRepository).save(order);
    }
    
    @Test
    void shouldNotSaveOrderWhenPaymentFails() {
        // Arrange
        Order order = new Order("123", 100.0);
        PaymentResult paymentResult = new PaymentResult(false, "Insufficient funds");
        
        when(paymentGateway.processPayment(100.0)).thenReturn(paymentResult);
        
        // Act
        OrderResult result = orderService.placeOrder(order);
        
        // Assert
        assertFalse(result.isSuccessful());
        assertEquals("Payment failed: Insufficient funds", result.getMessage());
        
        // Verify interactions
        verify(paymentGateway).processPayment(100.0);
        verify(orderRepository, never()).save(any(Order.class));
    }
}

Guided Project

Mastery Task 3: Writing Unit Tests with Mocks

Writing Unit Tests With Mocks

When writing unit tests, it is important to avoid calling any methods the belong to classes outside of the class we are testing. Since Spring requires coordination between several different layers even for the most basic API requests, mocking dependencies is very important.

For Spring, this means using the @MockBean annotation to create a mock of any component we would otherwise be @Autowiring in.

We can then use Mockito.when().thenReturn() structure to return dummy data instead of calling the real method of a dependent class. We can also use Mockito.verify() to check that a particular method did indeed get called. This is useful for void methods which have no return data, and thus we cannot see that they were called using Mockito.when().thenReturn().

Your task is to write unit tests for the CheckableService and achieve 95% or more code coverage. You will be given a jacoco gradle command to quickly test for code coverage, or you can select run with coverage on your test file to generate a detailed report (as an html file) under build/jacocoHtml.

Make sure to only @Autowire the component under test! All other dependencies must be mocked. Your test code will be tested using Reflection again so please make sure to continue following the naming conventions. You can use the LibraryServiceTest as a reference.

Completion

Run the gradle command:

./gradlew -q clean test jacocoTestCoverage

and make sure all tests pass. (This will also test MT01 and MT02)

Resources