1. 概述

在这个教程中,我们将探讨如何使用Mockito来模拟单例。

2. 项目设置

我们将创建一个使用单例模式的小项目,然后研究如何为使用该单例的类编写测试。

2.1. 依赖项 - JUnit & Mockito

首先,让我们在pom.xml中添加JUnitMockito 的依赖:

<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>5.11.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2. 代码示例

我们将创建一个内存缓存管理的单例CacheManager

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);
    }
}

为了简化,我们使用了一个更简单的单例实现,没有考虑多线程情况。

接下来,我们将创建一个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;
    }
}

getProduct()方法首先检查缓存中是否存在值。如果没有,它会调用DAO获取产品。

我们将为getProduct()方法编写测试。测试将检查如果产品在缓存中存在,是否不会调用DAO。为此,我们需要使cacheManager.getValue()方法在被调用时返回一个产品。

由于单例实例是由static getInstance()方法提供的,因此需要以不同的方式对其进行模拟和注入。让我们来看看几种实现方式。

3. 使用另一个构造函数的工作绕行

一种解决方法是在ProductService中添加另一个构造函数,使其易于注入模拟的单例CacheManager实例:

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

现在,让我们写一个利用这个构造函数并使用Mockito模拟CacheManager的测试:

@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());
}

这里需要注意几点:

  • 我们使用新构造函数注入了mocked的CacheManager实例。
  • 我们设置了cacheManager.getValue()方法在被调用时返回一个产品。
  • 在最后,我们验证了productDao.getProduct()方法在productService.getProduct()方法调用期间未被调用。

这种方法可以正常工作,但这并不是推荐的做法。编写测试不应要求我们在类中添加额外的方法或构造函数。

接下来,我们将看看另一种不需要修改正在测试的代码的方法。

4. 使用Mockito-inline模拟

另一种模拟单例缓存管理器的方法是模拟CacheManagerstatic方法getInstance()默认情况下,Mockito-core不支持模拟静态方法。但是,我们可以启用Mockito-inline扩展来模拟静态方法。

4.1. 启用Mockito-inline

一种启用使用Mockito模拟静态方法的方法是替换mockito-core依赖项为Mockito-inline

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

我们可以使用这个依赖关系替换mockito-core

另一种方法是配置Mockito以支持静态方法

4.2. 修改测试

让我们对测试做一些改动来模拟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());
    }
}

上述代码中需要注意的关键点:

  • 我们使用mockStatic()方法创建了一个CacheManager类的模拟版本。
  • 接下来,我们模拟了getInstance()方法,使其返回我们的CacheManager模拟实例。
  • 在模拟getInstance()方法后,我们创建了ProductService。当ProductService构造函数调用getInstance()时,将返回模拟的CacheManager实例。

测试按预期执行,因为模拟的缓存管理器返回了产品。

5. 总结

在这篇文章中,我们探讨了使用Mockito为单例编写单元测试的几种方法。我们首先看了一下通过构造函数传递模拟实例的解决方案。然后我们学习了如何使用Mockito-inline模拟static getInstance()方法。

如往常一样,本文中的代码示例可以在GitHub上找到


« 上一篇: MongoDB 过滤器指南
» 下一篇: Java Weekly, 第463期