1. 概述
在这个教程中,我们将探讨如何使用Mockito来模拟单例。
2. 项目设置
我们将创建一个使用单例模式的小项目,然后研究如何为使用该单例的类编写测试。
2.1. 依赖项 - JUnit & Mockito
首先,让我们在pom.xml中添加JUnit 和 Mockito 的依赖:
<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模拟
另一种模拟单例缓存管理器的方法是模拟CacheManager
的static
方法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上找到。