Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll explore how to mock a singleton using Mockito.

2. Project Setup

We’ll create a small project that uses a singleton and then look at how to write a test for the class that uses that singleton.

2.1. Dependencies – JUnit & Mockito

Let’s start by adding the JUnit and Mockito dependencies to our pom.xml:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.9.1</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>4.8.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2. Code Example

We’ll create a singleton CacheManager that manages in-memory caching:

public class CacheManager {
    private final HashMap<String, Object> map;
    private static CacheManager instance;

    private CacheManager() {
        map = new HashMap<>();
    }

    public static CacheManager getInstance() {
        if(instance == null) {
            instance = new CacheManager();
        }
        return instance;
    }

    public <T> T getValue(String key, Class<T> clazz) {
        return clazz.cast(map.get(key));
    }

    public Object setValue(String key, Object value) {
        return map.put(key, value);
    }
}

To keep it simple, we’ve used a simpler singleton implementation without considering multi-threaded cases.

Next, we’ll make a ProduceService:

public class ProductService {

    private final ProductDAO productDAO;
    private final CacheManager cacheManager;

    public ProductService(ProductDAO productDAO) {
        this.productDAO = productDAO;
        this.cacheManager = CacheManager.getInstance();
    }

    public Product getProduct(String productName) {
        Product product = cacheManager.getValue(productName, Product.class);
        if (product == null) {
            product = productDAO.getProduct(productName);
        }

        return product;
    }
}

The getProduct() method first checks if the value exists in the cache. If not, it calls the DAO to get the product.

We’ll write a test for the getProduct() method. The test will check that there are no calls to the DAO if the product is present in the cache. For this purpose, we want to make the cacheManager.getValue() method return a product when it’s called.

Since a singleton instance is provided by the static getInstance() method, it needs to be mocked and injected differently. Let’s look at a few ways to do this.

3. Workaround Using Another Constructor

One workaround is to add another constructor to ProductService that makes it easy to inject a mocked instance of the singleton CacheManager:

public ProductService(ProductDAO productDAO, CacheManager cacheManager) {
    this.productDAO = productDAO;
    this.cacheManager = cacheManager;
}

Let’s write a test that makes use of this constructor and mocks the CacheManager using Mockito:

@Test
void givenValueExistsInCache_whenGetProduct_thenDAOIsNotCalled() {
    ProductDAO productDAO = mock(ProductDAO.class);
    CacheManager cacheManager = mock(CacheManager.class);
    Product product = new Product("product1", "description");
    
    when(cacheManager.getValue(any(), any())).thenReturn(product);

    ProductService productService = new ProductService(productDAO, cacheManager);
    productService.getProduct("product1");

    Mockito.verify(productDAO, times(0)).getProduct(any());
}

A few important points to note here:

  • We mocked the cache manager and injected it into the ProductService using the new constructor.
  • We stubbed the cacheManager.getValue() method to return a product when it’s called.
  • In the end, we verified that the productDao.getProduct() method is not called during a call to productService.getProduct() method.

This works fine, but it wouldn’t be the recommended way to do this. Writing tests shouldn’t require us to create additional methods or constructors in our class.

Next, let’s look at another way that requires no change to the code being tested.

4. Mocking With Mockito-inline

Another way to mock the singleton cache manager is by mocking the static method CacheManager.getInstance(). Mockito-core doesn’t support the mocking of static methods by default. However, we can mock static methods by enabling the Mockito-inline extension.

4.1. Enabling Mockito-inline

One way to enable mocking static methods with Mockito is by adding the Mockito-inline dependency instead of Mockito-core:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.8.1</version>
    <scope>test</scope>
</dependency>

We can use this dependency to replace mockito-core.

Another way is by activating the inline Mock maker.

4.2. Modifying the Test

Let’s make some changes to our test to mock CacheManager:

@Test
void givenValueExistsInCache_whenGetProduct_thenDAOIsNotCalled_mockingStatic() {
    ProductDAO productDAO = mock(ProductDAO.class);
    CacheManager cacheManager = mock(CacheManager.class);
    Product product = new Product("product1", "description");

    try (MockedStatic<CacheManager> cacheManagerMock = mockStatic(CacheManager.class)) {
        cacheManagerMock.when(CacheManager::getInstance).thenReturn(cacheManager);
        when(cacheManager.getValue(any(), any())).thenReturn(product);
        
        ProductService productService = new ProductService(productDAO);
        productService.getProduct("product1");
        
        Mockito.verify(productDAO, times(0)).getProduct(any());
    }
}

A few important points to note in the above code:

  • We used the method mockStatic() to create a mocked version of the class CacheManager.
  • Next, we mocked the getInstance() method to return our mocked instance of CacheManager.
  • We’ve created the ProductService after mocking the getInstance() method. When the constructor of ProductService calls getInstance(), the mocked CacheManager instance will be returned.

The test executes as expected because the mocked cache manager returns the product.

5. Conclusion

In this article, we looked at a few ways to write unit tests for singletons using Mockito. We looked at a constructor-based workaround to pass a mocked instance. Then we looked at mocking the static getInstance() method using Mockito-inline.

As always, the code examples used in this article can be found over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!