Test-Driven Development (TDD) with Java and Mockito: A Complete Guide


Introduction
Test-Driven Development (TDD) is a software development methodology where tests are written before the actual implementation. This ensures that the code is thoroughly tested, maintainable, and meets requirements from the start.
In this blog, we’ll explore TDD with Java and Mockito, covering:
What is TDD?
Setting Up the Project
TDD Workflow (Red-Green-Refactor)
Mockito for Mocking Dependencies
End-to-End Example: Building a User Service
Best Practices
1. What is TDD?
TDD follows a simple cycle:
Red: Write a failing test.
Green: Write minimal code to pass the test.
Refactor: Improve the code while keeping tests passing.
Benefits of TDD:
✔ Fewer bugs
✔ Better design (loosely coupled, testable code)
✔ Documentation via tests
✔ Faster debugging
2. Setting Up the Project
We’ll use:
Java 11+
JUnit 5 (for testing)
Mockito (for mocking dependencies)
Maven Dependencies (pom.xml
)
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
</dependencies>
3. TDD Workflow (Red-Green-Refactor)
Let’s implement a simple Calculator
class using TDD.
Step 1: Red (Write a Failing Test)
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class CalculatorTest {
@Test
void add_twoNumbers_returnsSum() {
Calculator calculator = new Calculator();
int result = calculator.add(2, 3);
assertEquals(5, result); // Fails because Calculator doesn't exist
}
}
❌ Test fails (Red phase).
Step 2: Green (Implement Just Enough to Pass)
public class Calculator {
public int add(int a, int b) {
return a + b; // Minimal implementation
}
}
✅ Test passes (Green phase).
Step 3: Refactor (Improve Code Without Breaking Tests)
No refactoring needed here, but in complex cases, we might optimize or clean up.
4. Mockito for Mocking Dependencies
When testing classes with dependencies (e.g., databases, APIs), we use Mockito to mock them.
Example: Testing a UserService
with UserRepository
Step 1: Define the Test (Red Phase)
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository; // Mocked dependency
@InjectMocks
private UserService userService; // Injects mocks into UserService
@Test
void getUserById_userExists_returnsUser() {
// Arrange
User expectedUser = new User(1, "john@example.com");
when(userRepository.findById(1)).thenReturn(Optional.of(expectedUser));
// Act
User actualUser = userService.getUserById(1);
// Assert
assertEquals(expectedUser, actualUser);
verify(userRepository).findById(1); // Verify interaction
}
@Test
void getUserById_userNotFound_throwsException() {
// Arrange
when(userRepository.findById(1)).thenReturn(Optional.empty());
// Act & Assert
assertThrows(UserNotFoundException.class, () -> userService.getUserById(1));
}
}
❌ Fails because UserService
doesn’t exist yet.
Step 2: Implement UserService
(Green Phase)
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User not found"));
}
}
✅ Tests pass!
Step 3: Refactor (If Needed)
Improve exception handling
Optimize queries
5. End-to-End Example: Order Processing System
Let’s build an OrderService
with:
InventoryService
(checks stock)PaymentService
(processes payment)ShippingService
(schedules shipping)
Test Cases
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
@Mock private InventoryService inventoryService;
@Mock private PaymentService paymentService;
@Mock private ShippingService shippingService;
@InjectMocks private OrderService orderService;
@Test
void processOrder_successful_returnsSuccess() {
Order order = new Order("123", "item1", 2, 100.0);
when(inventoryService.checkStock("item1", 2)).thenReturn(true);
when(paymentService.processPayment(100.0)).thenReturn(true);
OrderResult result = orderService.processOrder(order);
assertTrue(result.isSuccess());
verify(shippingService).scheduleShipping("123");
}
@Test
void processOrder_outOfStock_returnsFailure() {
Order order = new Order("123", "item1", 2, 100.0);
when(inventoryService.checkStock("item1", 2)).thenReturn(false);
OrderResult result = orderService.processOrder(order);
assertFalse(result.isSuccess());
assertEquals("Out of stock", result.getMessage());
}
}
Implementation
public class OrderService {
private final InventoryService inventoryService;
private final PaymentService paymentService;
private final ShippingService shippingService;
public OrderService(InventoryService inventoryService,
PaymentService paymentService,
ShippingService shippingService) {
this.inventoryService = inventoryService;
this.paymentService = paymentService;
this.shippingService = shippingService;
}
public OrderResult processOrder(Order order) {
boolean inStock = inventoryService.checkStock(order.getItemId(), order.getQuantity());
if (!inStock) {
return new OrderResult(false, "Out of stock");
}
boolean paymentSuccess = paymentService.processPayment(order.getTotalAmount());
if (!paymentSuccess) {
return new OrderResult(false, "Payment failed");
}
shippingService.scheduleShipping(order.getOrderId());
return new OrderResult(true, "Order processed");
}
}
6. Best Practices
✅ Test behavior, not implementation (avoid over-specifying mocks).
✅ Keep tests small and focused (one assertion per test).
✅ Use descriptive test names (e.g., getUserById_userNotFound_throwsException
).
✅ Refactor tests along with production code.
✅ Avoid mocking everything (only mock external dependencies).
Conclusion
TDD with Java + Mockito leads to:
✔ Reliable code (tested from the start)
✔ Better design (forces modular, decoupled code)
✔ Easier debugging (failures are caught early)
Start with simple tests, mock dependencies, and gradually build complex systems with confidence! 🚀
Next Steps:
Try TDD in your next Java project
Explore Mockito’s advanced features (argument captors, spies)
Learn integration testing with Spring Boot
Subscribe to my newsletter
Read articles from Kolluru Pawan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
