Mocking and Test Doubles in Java

In the world of unit testing, ensuring that our tests are both effective and efficient is crucial. One of the most powerful techniques to achieve this is through the use of mocking and test doubles. In this article, we'll explore how to use the popular Java mocking framework, Mockito, to create test doubles and simplify our unit testing processes.

What are Test Doubles?

Before diving into Mockito, let’s clarify what a test double is. A test double is a generic term for any case where you replace a production object with a simpler version for the purpose of testing. There are four main types of test doubles:

  1. Dummy: These are objects that are passed around but never actually used. They exist only to satisfy parameter requirements.

  2. Fake: A fake is a working implementation, but it is usually a simpler version that is not suitable for production. For example, an in-memory database can serve as a fake database.

  3. Stub: A stub is a controllable replacement for a collaborator that returns predetermined data. Stubs are primarily used to specify what the collaborator will return for specific inputs.

  4. Mock: A mock is a verifying test double that not only acts like a stub but also verifies interactions. Mocks allow you to set expectations about how they are called.

In this article, we'll primarily focus on mocks and stubs, using Mockito.

Setting Up Mockito

To start using Mockito in your Java project, you need to include it as a dependency. If you are using Maven, you can add the following to your pom.xml:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>4.0.0</version> <!-- Check for the latest version on Maven Repository -->
    <scope>test</scope>
</dependency>

If you're using Gradle, add the following line to your build.gradle file:

testImplementation 'org.mockito:mockito-core:4.0.0' // Check for the latest version

Creating Mocks with Mockito

Mockito provides a simple and powerful API to create mocks. To illustrate its use, consider an application where we have a service that relies on a repository to interact with a database.

Example Classes

public class User {
    private String name;
    // constructor, getters, and setters
}

public interface UserRepository {
    User findById(String id);
    void save(User user);
}

public class UserService {
    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUser(String id) {
        return userRepository.findById(id);
    }

    public void createUser(User user) {
        userRepository.save(user);
    }
}

Writing Tests with Mocks

Let’s write a test for the UserService class using mocks for the UserRepository.

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

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

public class UserServiceTest {

    @Test
    public void testGetUser() {
        // Create a mock of UserRepository
        UserRepository userRepositoryMock = mock(UserRepository.class);
        
        // Set up mock behavior
        User user = new User("John Doe");
        when(userRepositoryMock.findById("1")).thenReturn(user);

        // Create UserService with the mock
        UserService userService = new UserService(userRepositoryMock);

        // Call the method under test
        User result = userService.getUser("1");

        // Verify the behavior
        assertEquals("John Doe", result.getName());
        verify(userRepositoryMock).findById("1");
    }

    @Test
    public void testCreateUser() {
        // Create a mock for UserRepository
        UserRepository userRepositoryMock = mock(UserRepository.class);
        
        // Create UserService with the mock
        UserService userService = new UserService(userRepositoryMock);
        
        // Prepare a User object
        User user = new User("Jane Doe");

        // Call the method under test
        userService.createUser(user);
        
        // Verify the interaction
        verify(userRepositoryMock).save(user);
    }
}

Explanation

  1. Creating a Mock:

    • The mock(UserRepository.class) method creates a mock instance of the UserRepository.
  2. Stubbing Methods:

    • The when(...).thenReturn(...) construct is used to specify the behavior of the mock. Here, when findById("1") is called on userRepositoryMock, it returns a predefined user.
  3. Testing the Method:

    • We call the method getUser, and then check whether the result matches our expectations.
  4. Verifying Interactions:

    • The verify(...) method ensures that specific methods were called on the mock object, which can be invaluable for checking interactions in your unit tests.

Real-World Scenario: Throwing Exceptions

Sometimes you might want to test how your code behaves when a method call on a mock throws an exception. Mockito allows you to do this easily.

@Test
public void testGetUserNotFound() {
    UserRepository userRepositoryMock = mock(UserRepository.class);
    when(userRepositoryMock.findById("non-existent")).thenThrow(new RuntimeException("User not found"));

    UserService userService = new UserService(userRepositoryMock);

    Exception exception = assertThrows(RuntimeException.class, () -> {
        userService.getUser("non-existent");
    });

    assertEquals("User not found", exception.getMessage());
}

Conclusion

Mocking and using test doubles are powerful strategies that can improve the quality and readability of your unit tests. With Mockito, you can easily create mocks and simulate various scenarios, ensuring that your code behaves as expected under different conditions.

As you build more complex applications, taking advantage of these tools will not only help you achieve better test coverage but will also simplify the process of isolating components for unit testing. Happy testing!