Introduction to Mocking
Mocking is a technique used in unit testing to isolate the code being tested by replacing dependencies with mock objects. This allows you to test your code in isolation, without being affected by external factors such as databases, web services, or other complex components.
Why Use Mocking?
There are several reasons to use mocking in your tests:
- Isolation: Test your code in isolation from its dependencies
- Speed: Tests run faster without real dependencies
- Reliability: Tests are more reliable when they don't depend on external systems
- Control: You can control the behavior of dependencies to test different scenarios
- Verification: You can verify that your code interacts correctly with its dependencies
Mockito Integration with TestNG
Mockito is one of the most popular mocking frameworks for Java. It integrates seamlessly with TestNG to provide powerful mocking capabilities.
Setting Up Mockito
To use Mockito with TestNG, you need to add the Mockito dependency to your project:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version> <!-- Use the latest version -->
<scope>test</scope>
</dependency>
Basic Mocking with Mockito
Here's a simple example of using Mockito with TestNG:
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static org.mockito.Mockito.*;
public class BasicMockingExample {
// The class we want to test
private UserService userService;
// The dependency we want to mock
@Mock
private UserRepository userRepository;
@BeforeMethod
public void setup() {
// Initialize mocks
MockitoAnnotations.openMocks(this);
// Create the service with the mock repository
userService = new UserService(userRepository);
}
@Test
public void testGetUserById() {
// Arrange
User expectedUser = new User(1, "John Doe", "john@example.com");
when(userRepository.findById(1)).thenReturn(expectedUser);
// Act
User actualUser = userService.getUserById(1);
// Assert
Assert.assertEquals(actualUser, expectedUser, "User should match");
// Verify that the repository method was called with the correct argument
verify(userRepository).findById(1);
}
}
// Classes used in the example
class User {
private int id;
private String name;
private String email;
public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters and equals/hashCode methods omitted for brevity
}
interface UserRepository {
User findById(int id);
void save(User user);
}
class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.findById(id);
}
public void saveUser(User user) {
userRepository.save(user);
}
}
Stubbing Method Calls
You can stub method calls to make your mocks return specific values:
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
import static org.mockito.Mockito.*;
public class StubbingExample {
@Mock
private UserRepository userRepository;
private UserService userService;
@BeforeMethod
public void setup() {
MockitoAnnotations.openMocks(this);
userService = new UserService(userRepository);
}
@Test
public void testGetAllUsers() {
// Arrange
List<User> expectedUsers = Arrays.asList(
new User(1, "John Doe", "john@example.com"),
new User(2, "Jane Smith", "jane@example.com")
);
when(userRepository.findAll()).thenReturn(expectedUsers);
// Act
List<User> actualUsers = userService.getAllUsers();
// Assert
Assert.assertEquals(actualUsers, expectedUsers, "Users should match");
verify(userRepository).findAll();
}
@Test
public void testGetUserByEmail() {
// Arrange
User expectedUser = new User(1, "John Doe", "john@example.com");
when(userRepository.findByEmail("john@example.com")).thenReturn(expectedUser);
when(userRepository.findByEmail("nonexistent@example.com")).thenReturn(null);
// Act
User actualUser1 = userService.getUserByEmail("john@example.com");
User actualUser2 = userService.getUserByEmail("nonexistent@example.com");
// Assert
Assert.assertEquals(actualUser1, expectedUser, "User should match");
Assert.assertNull(actualUser2, "User should be null");
verify(userRepository).findByEmail("john@example.com");
verify(userRepository).findByEmail("nonexistent@example.com");
}
}
// Additional methods for the classes
interface UserRepository {
User findById(int id);
User findByEmail(String email);
List<User> findAll();
void save(User user);
}
class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(int id) {
return userRepository.findById(id);
}
public User getUserByEmail(String email) {
return userRepository.findByEmail(email);
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public void saveUser(User user) {
userRepository.save(user);
}
}
Verifying Interactions
You can verify that your code interacts correctly with its dependencies:
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static org.mockito.Mockito.*;
public class VerificationExample {
@Mock
private UserRepository userRepository;
private UserService userService;
@BeforeMethod
public void setup() {
MockitoAnnotations.openMocks(this);
userService = new UserService(userRepository);
}
@Test
public void testSaveUser() {
// Arrange
User user = new User(1, "John Doe", "john@example.com");
// Act
userService.saveUser(user);
// Verify that the repository's save method was called with the correct user
verify(userRepository).save(user);
}
@Test
public void testDeleteUser() {
// Arrange
int userId = 1;
// Act
userService.deleteUser(userId);
// Verify that the repository's delete method was called with the correct ID
verify(userRepository).deleteById(userId);
}
@Test
public void testUpdateUser() {
// Arrange
User user = new User(1, "John Doe", "john@example.com");
// Act
userService.updateUser(user);
// Verify that the repository's update method was called with the correct user
verify(userRepository).update(user);
// Verify that no other methods were called on the repository
verifyNoMoreInteractions(userRepository);
}
@Test
public void testGetUserByIdNeverCalled() {
// Verify that a method was never called
verify(userRepository, never()).findById(anyInt());
}
}
Advanced Mockito Features
Mockito provides advanced features for more complex testing scenarios.
Argument Matchers
You can use argument matchers to make your stubs and verifications more flexible:
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
public class ArgumentMatchersExample {
@Mock
private UserRepository userRepository;
private UserService userService;
@BeforeMethod
public void setup() {
MockitoAnnotations.openMocks(this);
userService = new UserService(userRepository);
}
@Test
public void testFindUserWithMatchers() {
// Arrange
User expectedUser = new User(1, "John Doe", "john@example.com");
// Using argument matchers
when(userRepository.findById(anyInt())).thenReturn(expectedUser);
when(userRepository.findByEmail(contains("@example.com"))).thenReturn(expectedUser);
// Act
User user1 = userService.getUserById(1);
User user2 = userService.getUserById(999);
User user3 = userService.getUserByEmail("john@example.com");
User user4 = userService.getUserByEmail("jane@example.com");
// Assert
Assert.assertEquals(user1, expectedUser, "User should match");
Assert.assertEquals(user2, expectedUser, "User should match");
Assert.assertEquals(user3, expectedUser, "User should match");
Assert.assertEquals(user4, expectedUser, "User should match");
}
@Test
public void testSaveUserWithMatchers() {
// Act
userService.saveUser(new User(1, "John Doe", "john@example.com"));
// Verify with argument matchers
verify(userRepository).save(argThat(user ->
user.getName().equals("John Doe") &&
user.getEmail().equals("john@example.com")
));
}
@Test
public void testArgumentCaptor() {
// Arrange
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
User user = new User(1, "John Doe", "john@example.com");
// Act
userService.saveUser(user);
// Verify and capture the argument
verify(userRepository).save(userCaptor.capture());
// Assert on the captured argument
User capturedUser = userCaptor.getValue();
Assert.assertEquals(capturedUser.getId(), 1, "User ID should match");
Assert.assertEquals(capturedUser.getName(), "John Doe", "User name should match");
Assert.assertEquals(capturedUser.getEmail(), "john@example.com", "User email should match");
}
}
// Updated User class with getters
class User {
private int id;
private String name;
private String email;
public User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
// equals and hashCode methods omitted for brevity
}
Spying on Real Objects
Sometimes you want to use a real object but still track interactions with it:
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import java.util.ArrayList;
import java.util.List;
import static org.mockito.Mockito.*;
public class SpyExample {
@Spy
private List<String> spyList = new ArrayList<>();
@BeforeMethod
public void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testSpyList() {
// Using the real method
spyList.add("one");
spyList.add("two");
// Verify the real methods were called
verify(spyList).add("one");
verify(spyList).add("two");
// The real list was modified
Assert.assertEquals(spyList.size(), 2, "List should have 2 items");
Assert.assertEquals(spyList.get(0), "one", "First item should be 'one'");
// Stub a method
when(spyList.size()).thenReturn(100);
// Now the stubbed method returns the stubbed value
Assert.assertEquals(spyList.size(), 100, "Stubbed size should be 100");
// But other methods still work normally
Assert.assertEquals(spyList.get(1), "two", "Second item should be 'two'");
}
@Test
public void testSpyWithDoReturn() {
// Add some items
spyList.add("one");
spyList.add("two");
// This would throw an exception because get(5) is out of bounds
// spyList.get(5);
// But we can stub it to return a value
doReturn("stubbed value").when(spyList).get(5);
// Now it returns the stubbed value
Assert.assertEquals(spyList.get(5), "stubbed value", "Should return stubbed value");
}
}
Mocking Static Methods
With Mockito 3.4.0 and above, you can mock static methods:
import org.mockito.MockedStatic;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.time.LocalDate;
import static org.mockito.Mockito.*;
public class StaticMethodMockingExample {
@Test
public void testMockingStaticMethod() {
// Define a fixed date for testing
LocalDate fixedDate = LocalDate.of(2025, 1, 1);
// Mock the static method
try (MockedStatic<LocalDate> mockedLocalDate = mockStatic(LocalDate.class)) {
// Stub the static method
mockedLocalDate.when(LocalDate::now).thenReturn(fixedDate);
// Use a class that depends on LocalDate.now()
DateProvider dateProvider = new DateProvider();
LocalDate currentDate = dateProvider.getCurrentDate();
// Verify the result
Assert.assertEquals(currentDate, fixedDate, "Current date should match fixed date");
// Verify the static method was called
mockedLocalDate.verify(LocalDate::now);
}
}
}
class DateProvider {
public LocalDate getCurrentDate() {
return LocalDate.now();
}
public boolean isWeekend(LocalDate date) {
return date.getDayOfWeek().getValue() >= 6;
}
}