值得收藏!Java单元测试典型案例集锦
前言
1、无效验证问题:
不进行有效地验证数据对象、抛出异常和调用方法。
2、测试方法问题:
不知道如何测试某些典型案例,要么错误地测试、要么不进行测试、要么利用集成测试来保证覆盖率。比如:
错误地测试:利用测试返回节点占比来测试随机负载均衡策略;
不进行测试:没有人针对虚基类进行单独地测试;
利用集成测试:很多案例中,直接注入真实依赖对象,然后一起进行集成测试。
一、如何测试不可达代码
1.1. 案例代码
/**
* 交易订单服务类
*/
@Service
public class TradeOrderService {
/** 注入依赖对象 */
/** 交易订单DAO */
@Autowired
private TradeOrderDAO tradeOrderDAO;
/**
* 查询交易订单
*
* @param orderQuery 订单查询
* @return 交易订单分页
*/
public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) {
// 查询交易订单
// 查询交易订单: 总共数量
Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
// 查询交易订单: 数据列表
List<TradeOrderVO> dataList = null;
if (NumberHelper.isPositive(totalSize)) {
List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
if (CollectionUtils.isNotEmpty(tradeOrderList)) {
dataList = convertTradeOrders(tradeOrderList);
}
}
// 返回分页数据
return new PageDataVO<>(totalSize, dataList);
}
/**
* 转化交易订单列表
*
* @param tradeOrderList 交易订单DO列表
* @return 交易订单VO列表
*/
private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) {
// 检查订单列表
if (CollectionUtils.isEmpty(tradeOrderList)) {
return Collections.emptyList();
}
// 转化订单列表
return tradeOrderList.stream().map(TradeOrderService::convertTradeOrder)
.collect(Collectors.toList());
}
/**
* 转化交易订单
*
* @param tradeOrder 交易订单DO
* @return 交易订单VO
*/
private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) {
TradeOrderVO tradeOrderVO = new TradeOrderVO();
tradeOrderVO.setId(tradeOrder.getId());
// ...
return tradeOrderVO;
}
}
// 检查订单列表
if (CollectionUtils.isEmpty(tradeOrderList)) {
return Collections.emptyList();
}
/**
* 转化交易订单列表
*
* @param tradeOrderList 交易订单DO列表
* @return 交易订单VO列表
*/
private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) {
return tradeOrderList.stream().map(TradeOrderService2::convertTradeOrder)
.collect(Collectors.toList());
}
1.3. 方案2:利用不可达代码(推荐)
/**
* 查询交易订单
*
* @param orderQuery 订单查询
* @return 交易订单分页
*/
public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) {
// 查询交易订单
// 查询交易订单: 总共数量
Long totalSize = tradeOrderDAO.countByCondition(orderQuery);
// 查询交易订单: 数据列表
List<TradeOrderVO> dataList = null;
if (NumberHelper.isPositive(totalSize)) {
List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery);
dataList = convertTradeOrders(tradeOrderList);
}
// 返回分页数据
return new PageDataVO<>(totalSize, dataList);
}
1.4. 方案3:测试不可达代码(不推荐)
/**
* 测试: 转化交易订单列表-交易订单列表为空
*
* @throws Exception 异常信息
*/
public void testConvertTradeOrdersWithTradeOrderListEmpty() throws Exception {
List<TradeOrderDO> tradeOrderList = null;
Assert.assertSame("交易订单列表不为空", Collections.emptyList(),
Whitebox.invokeMethod(TradeOrderService1.class, "convertTradeOrders", tradeOrderList));
}
二、如何测试内部的构造方法
2.1. 代码案例
/**
* 随机负载均衡策略类
*/
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 检查节点列表
if (CollectionUtils.isEmpty(serverNodeList)) {
return null;
}
// 计算随机序号
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
int randomIndex = new Random().nextInt(totalWeight);
// 查找对应节点
for (ServerNode serverNode : serverNodeList) {
int currentWeight = serverNode.getWeight();
if (currentWeight > randomIndex) {
return serverNode;
}
randomIndex -= currentWeight;
}
return null;
}
}
/**
* 随机负载均衡策略测试类
*/
(MockitoJUnitRunner.class)
public class RandomLoadBalanceStrategyTest {
/** 定义测试对象 */
/** 随机负载均衡策略 */
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-随机
*
* @throws Exception 异常信息
*/
public void testSelectNodeWithRandom() throws Exception {
int nodeCount1 = 0;
int nodeCount2 = 0;
int nodeCount3 = 0;
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
for (int i = 0; i < 1000; i++) {
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
if (serviceNode == serverNode1) {
nodeCount1++;
} else if (serviceNode == serverNode2) {
nodeCount2++;
} else if (serviceNode == serverNode3) {
nodeCount3++;
}
}
Assert.assertEquals("节点1占比不一致", serverNode1.getWeight() / 60.0D, nodeCount1 / 1000.0D, 1E-3D);
Assert.assertEquals("节点2占比不一致", serverNode2.getWeight() / 60.0D, nodeCount2 / 1000.0D, 1E-3D);
Assert.assertEquals("节点3占比不一致", serverNode3.getWeight() / 60.0D, nodeCount3 / 1000.0D, 1E-3D);
}
}
执行时间长:被测方法需要被执行1000遍;
不一定通过:由于随机数是随机,并不一定保证比例,所以导致测试用例并不一定通过;
测试目标变更:单测测试的测试目标应该是负载均衡逻辑,现在感觉测试目标变成了Random方法。
2.3. 方法2:直接mock法(不推荐)
/**
* 随机负载均衡策略测试类
*/
(PowerMockRunner.class)
(RandomLoadBalanceStrategy.class)
public class RandomLoadBalanceStrategyTest {
/** 定义测试对象 */
/** 随机负载均衡策略 */
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-第一个节点
*
* @throws Exception 异常信息
*/
public void testSelectNodeWithFirstNode() throws Exception {
// 模拟依赖方法
Random random = Mockito.mock(Random.class);
Mockito.doReturn(9).when(random).nextInt(Mockito.anyInt());
PowerMockito.whenNew(Random.class).withNoArguments().thenReturn(random);
// 调用测试方法
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
// 验证依赖方法
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
Mockito.verify(random).nextInt(totalWeight);
}
}
2.4. 方法3:工具方法法(推荐)
2.4.1. 重构代码
/**
* 随机负载均衡策略类
*/
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 检查节点列表
if (CollectionUtils.isEmpty(serverNodeList)) {
return null;
}
// 计算随机序号
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
int randomIndex = RandomUtils.nextInt(0, totalWeight);
// 查找对应节点
for (ServerNode serverNode : serverNodeList) {
int currentWeight = serverNode.getWeight();
if (currentWeight > randomIndex) {
return serverNode;
}
randomIndex -= currentWeight;
}
return null;
}
}
2.4.2. 测试用例
/**
* 随机负载均衡策略测试类
*/
(PowerMockRunner.class)
(RandomUtils.class)
public class RandomLoadBalanceStrategyTest {
/** 定义测试对象 */
/** 随机负载均衡策略 */
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-第一个节点
*/
public void testSelectNodeWithFirstNode() {
// 模拟依赖方法
PowerMockito.mockStatic(RandomUtils.class);
PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9);
// 调用测试方法
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
// 验证依赖方法
PowerMockito.verifyStatic(RandomUtils.class);
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
RandomUtils.nextInt(0, totalWeight);
}
}
2.5. 方法4:注入对象法(推荐)
2.5.1. 重构代码
/**
* 随机负载均衡策略类
*/
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
/** 注入依赖对象 */
/** 随机数提供者 */
private RandomProvider randomProvider;
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 检查节点列表
if (CollectionUtils.isEmpty(serverNodeList)) {
return null;
}
// 计算随机序号
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
int randomIndex = randomProvider.nextInt(totalWeight);
// 查找对应节点
for (ServerNode serverNode : serverNodeList) {
int currentWeight = serverNode.getWeight();
if (currentWeight > randomIndex) {
return serverNode;
}
randomIndex -= currentWeight;
}
return null;
}
}
2.5.2. 测试用例
/**
* 随机负载均衡策略测试类
*/
(MockitoJUnitRunner.class)
public class RandomLoadBalanceStrategyTest {
/** 模拟依赖方法 */
/** 随机数提供者 */
private RandomProvider randomProvider;
/** 定义测试对象 */
/** 随机负载均衡策略 */
private RandomLoadBalanceStrategy randomLoadBalanceStrategy;
/**
* 测试: 选择服务节点-第一个节点
*/
public void testSelectNodeWithFirstNode() {
// 模拟依赖方法
Mockito.doReturn(9).when(randomProvider).nextInt(Mockito.anyInt());
// 调用测试方法
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest);
Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
// 验证依赖方法
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
Mockito.verify(randomProvider).nextInt(totalWeight);
}
}
三、如何测试虚基类和子类
3.1. 案例代码
3.1.1. 虚基类定义
/**
* 虚属性回调类
*
* @param <T> 配置类型
*/
public abstract class AbstractPropertiesCallback<T> implements DiamondDataCallback {
/** 注入依赖对象 */
/** 环境 */
private Environment environment;
/** 转化服务 */
private ConversionService conversionService;
/**
* 接收到数据
*
* @param data 配置数据
*/
public void received(String data) {
// 获取配置参数
String configName = getConfigName();
Assert.notNull(configName, "配置名称不能为空");
T configInstance = getConfigInstance();
Assert.notNull(configInstance, "配置实例不能为空");
// 解析配置数据
try {
log.info("绑定属性配置文件开始: configName={}", configName);
Properties properties = new Properties();
byte[] bytes = Optional.ofNullable(data.getBytes()).orElseGet(() -> new byte[0]);
InputStream inputStream = new ByteArrayInputStream(bytes);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
properties.load(bufferedReader);
Bindable<T> bindable = Bindable.ofInstance(configInstance);
Binder binder = new Binder(ConfigurationPropertySources.from(
new PropertiesPropertySource(configName, properties)),
new PropertySourcesPlaceholdersResolver(environment), conversionService);
BindResult<T> result = binder.bind(configName, bindable);
if (!result.isBound()) {
log.error("绑定属性配置文件失败: configName={}", configName);
return;
}
log.info("绑定属性配置文件成功: configName={}, configInstance={}", configName, JSON.toJSONString(configInstance));
} catch (IOException | RuntimeException e) {
log.error("绑定属性配置文件异常: configName={}", configName, e);
}
}
/**
* 获取配置名称
*
* @return 配置名称
*/
protected abstract String getConfigName();
/**
* 获取配置实例
*
* @return 配置实例
*/
protected abstract T getConfigInstance();
}
3.1.2. 子类实现
/**
* 例子配置回调类
*/
"unittest-example", dataId = "example.properties", executeAfterInit = true) (groupId =
public class ExampleConfigCallback extends AbstractPropertiesCallback<ExampleConfig> {
/** 注入依赖对象 */
/** 例子配置 */
private ExampleConfig exampleConfig;
/**
* 获取配置名称
*
* @return 配置名称
*/
protected String getConfigName() {
return "example";
}
/**
* 获取配置实例
*
* @return 配置实例
*/
protected ExampleConfig getConfigInstance() {
return exampleConfig;
}
}
3.2. 方法1:联合测试法(不推荐)
/**
* 例子配置回调测试类
*/
(MockitoJUnitRunner.class)
public class ExampleConfigCallbackTest {
/** 定义静态常量 */
/** 资源路径 */
private static final String RESOURCE_PATH = "testExampleConfigCallback/";
/** 模拟依赖对象 */
/** 配置环境 */
private ConfigurableEnvironment environment;
/** 转化服务 */
private ConversionService conversionService;
/** 定义测试对象 */
/** BOSS取消费配置回调 */
private ExampleConfigCallback exampleConfigCallback;
/**
* 测试: 接收-正常
*/
public void testReceivedWithNormal() {
// 模拟依赖对象
ExampleConfig exampleConfig = new ExampleConfig();
Whitebox.setInternalState(exampleConfigCallback, "exampleConfig", exampleConfig);
// 调用测试方法
String text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties");
exampleConfigCallback.received(text);
// 验证依赖对象
text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json");
Assert.assertEquals("取消费用配置不一致", text, JSON.toJSONString(exampleConfig, SerializerFeature.MapSortField));
}
}
3.3. 方法2:独立测试法(推荐)
3.3.1. 基类测试
/**
* 虚属性回调测试类
*/
(MockitoJUnitRunner.class)
public class AbstractPropertiesCallbackTest {
/** 静态常量相关 */
/** 资源目录 */
private static final String RESOURCE_PATH = "testAbstractPropertiesCallback/";
/** 模拟依赖对象 */
/** 环境 */
private ConfigurableEnvironment environment;
/** 转化服务 */
private ConversionService conversionService;
/** 定义测试对象 */
/** 虚属性回调 */
private AbstractPropertiesCallback<ExampleConfig> propertiesCallback =
CastUtils.cast(Mockito.spy(AbstractPropertiesCallback.class));
/**
* 测试: 接收到-正常
*/
public void testReceivedWithNormal() {
// 模拟依赖方法
// 模拟依赖方法: propertiesCallback.getConfigName
String configName = "example";
Mockito.doReturn(configName).when(propertiesCallback).getConfigName();
// 模拟依赖方法: propertiesCallback.getConfigInstance
ExampleConfig configInstance = new ExampleConfig();
Mockito.doReturn(configInstance).when(propertiesCallback).getConfigInstance();
// 调用测试方法
String text1 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties");
propertiesCallback.received(text1);
String text2 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json");
Assert.assertEquals("任务配置不一致", text2, JSON.toJSONString(configInstance));
// 验证依赖方法
// 验证依赖方法: propertiesCallback.received
Mockito.verify(propertiesCallback).received(text1);
// 验证依赖方法: propertiesCallback.getConfigName
Mockito.verify(propertiesCallback).getConfigName();
// 验证依赖方法: propertiesCallback.getConfigInstance
Mockito.verify(propertiesCallback).getConfigInstance();
}
}
3.3.2. 子类测试
/**
* 例子配置回调测试类
*/
@RunWith(MockitoJUnitRunner.class)
public class ExampleConfigCallbackTest {
/** 定义测试对象 */
/** BOSS取消费配置回调 */
@InjectMocks
private ExampleConfigCallback exampleConfigCallback;
/**
* 测试: 获取配置实例
*/
@Test
public void testGetConfigInstance() {
Assert.assertEquals("配置实例不一致", exampleConfig, exampleConfigCallback.getConfigInstance());
}
/**
* 测试: 获取配置名称
*/
@Test
public void testGetConfigName() {
Assert.assertEquals("配置名称不一致", "example", exampleConfigCallback.getConfigName());
}
}
四、如何测试策略模式的策略服务
4.1. 案例代码
4.1.1. 策略接口
/**
* 负载均衡策略接口
*/
public interface LoadBalanceStrategy {
/**
* 支持策略类型
*
* @return 策略类型
*/
LoadBalanceStrategyType supportType();
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest);
}
4.1.2. 策略服务
/**
* 负载均衡服务类
*/
public class LoadBalanceService {
/** 负载均衡策略映射 */
private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap;
/**
* 构造方法
*
* @param strategyList 负载均衡策略列表
*/
public LoadBalanceService(List<LoadBalanceStrategy> strategyList) {
strategyMap = new EnumMap<>(LoadBalanceStrategyType.class);
for (LoadBalanceStrategy strategy : strategyList) {
strategyMap.put(strategy.supportType(), strategy);
}
}
/**
* 选择服务节点
*
* @param strategyType 策略类型
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
public ServerNode selectNode(LoadBalanceStrategyType strategyType,
List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 获取负载均衡策略
LoadBalanceStrategy strategy = strategyMap.get(strategyType);
if (Objects.isNull(strategy)) {
throw new BusinessException("负载均衡策略不存在");
}
// 执行负载均衡策略
return strategy.selectNode(serverNodeList, clientRequest);
}
}
4.1.3. 策略实现
/**
* 随机负载均衡策略类
*/
public class RandomLoadBalanceStrategy implements LoadBalanceStrategy {
/**
* 支持策略类型
*
* @return 策略类型
*/
public LoadBalanceStrategyType supportType() {
return LoadBalanceStrategyType.RANDOM;
}
/**
* 选择服务节点
*
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 检查节点列表
if (CollectionUtils.isEmpty(serverNodeList)) {
return null;
}
// 计算随机序号
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
int randomIndex = RandomUtils.nextInt(0, totalWeight);
// 查找对应节点
for (ServerNode serverNode : serverNodeList) {
int currentWeight = serverNode.getWeight();
if (currentWeight > randomIndex) {
return serverNode;
}
randomIndex -= currentWeight;
}
return null;
}
}
4.2. 方法1:联合测试法(不推荐)
/**
* 负载均衡服务测试类
*/
(PowerMockRunner.class)
(RandomUtils.class)
public class LoadBalanceServiceTest {
/**
* 测试: 选择服务节点-正常
*/
public void testSelectNodeWithNormal() {
// 模拟依赖方法
PowerMockito.mockStatic(RandomUtils.class);
PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9);
// 调用测试方法
ServerNode serverNode1 = new ServerNode(1L, 10);
ServerNode serverNode2 = new ServerNode(2L, 20);
ServerNode serverNode3 = new ServerNode(3L, 30);
List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3);
ClientRequest clientRequest = new ClientRequest();
RandomLoadBalanceStrategy randomLoadBalanceStrategy = new RandomLoadBalanceStrategy();
LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(randomLoadBalanceStrategy));
ServerNode serviceNode = loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM,
serverNodeList, clientRequest);
Assert.assertEquals("服务节点不一致", serverNode1, serviceNode);
// 验证依赖方法
PowerMockito.verifyStatic(RandomUtils.class);
int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum();
RandomUtils.nextInt(0, totalWeight);
}
}
策略服务依赖于策略实现,需要了解策略实现的具体逻辑,才能写出策略服务的单元测试;
对于策略服务来说,该单元测试并不关心策略服务的实现,这是黑盒测试而不是白盒测试。
strategyMap没有根据strategyList生成;
strategyMap.get(strategyType)为空时,初始化一个RandomLoadBalanceStrategy。
/**
* 负载均衡服务类
*/
public class LoadBalanceService {
/** 负载均衡策略映射 */
private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap;
/**
* 构造方法
*
* @param strategyList 负载均衡策略列表
*/
public LoadBalanceService(List<LoadBalanceStrategy> strategyList) {
strategyMap = new EnumMap<>(LoadBalanceStrategyType.class);
}
/**
* 选择服务节点
*
* @param strategyType 策略类型
* @param serverNodeList 服务节点列表
* @param clientRequest 客户请求
* @return 服务节点
*/
public ServerNode selectNode(LoadBalanceStrategyType strategyType,
List<ServerNode> serverNodeList, ClientRequest clientRequest) {
// 获取负载均衡策略
LoadBalanceStrategy strategy = strategyMap.get(strategyType);
if (Objects.isNull(strategy)) {
strategy = new RandomLoadBalanceStrategy();
}
// 执行负载均衡策略
return strategy.selectNode(serverNodeList, clientRequest);
}
}
4.3. 方法2:独立测试法(推荐)
/**
* 负载均衡服务测试类
*/
public class LoadBalanceServiceTest {
/**
* 测试: 构造方法
*/
public void testConstructor() {
// 模拟依赖方法
LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class);
Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType();
// 调用测试方法
LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy));
Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap =
Whitebox.getInternalState(loadBalanceService, "strategyMap");
Assert.assertEquals("策略映射大小不一致", 1, strategyMap.size());
Assert.assertEquals("策略映射对象不一致", loadBalanceStrategy, strategyMap.get(LoadBalanceStrategyType.RANDOM));
// 验证依赖方法
Mockito.verify(loadBalanceStrategy).supportType();
}
/**
* 测试: 选择服务节点-正常
*/
public void testSelectNodeWithNormal() {
// 模拟依赖方法
LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class);
// 模拟依赖方法: loadBalanceStrategy.supportType
Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType();
// 模拟依赖方法: loadBalanceStrategy.selectNode
ServerNode serverNode = Mockito.mock(ServerNode.class);
Mockito.doReturn(serverNode).when(loadBalanceStrategy)
.selectNode(Mockito.anyList(), Mockito.any(ClientRequest.class));
// 调用测试方法
List<ServerNode> serverNodeList = CastUtils.cast(Mockito.mock(List.class));
ClientRequest clientRequest = Mockito.mock(ClientRequest.class);
LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy));
Assert.assertEquals("服务节点不一致", serverNode,
loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM, serverNodeList, clientRequest));
// 验证依赖方法
// 验证依赖方法: loadBalanceStrategy.supportType
Mockito.verify(loadBalanceStrategy).supportType();
// 验证依赖方法: loadBalanceStrategy.selectNode
Mockito.verify(loadBalanceStrategy).selectNode(serverNodeList, clientRequest);
}
}
五、如何测试Lambda表达式
5.1. 案例代码
5.1.1. 被测代码
/**
* 交易订单服务
*/
public class TradeOrderService {
/** 注入依赖对象 */
/** 交易ODPS服务 */
private TradeOdpsService tradeOdpsService;
/**
* 查询交易订单
*
* @param userId 用户标识
* @param maxCount 最大数量
* @return 交易订单列表
*/
public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) {
String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql");
String sql = String.format(format, userId, maxCount);
return tradeOdpsService.executeQuery(sql, record -> {
TradeOrderVO tradeOrder = new TradeOrderVO();
tradeOrder.setId(record.getBigint("id"));
// ...
return tradeOrder;
});
}
}
5.1.2. 依赖代码
/**
* 交易ODPS服务类
*/
@Slf4j
@Service
public class TradeOdpsService {
/** 注入依赖对象 */
/** 交易ODPS */
@Resource(name = "tradeOdps")
private Odps tradeOdps;
/**
* 执行查询
*
* @param <T> 模板类型
* @param sql SQL语句
* @param dataParser 数据解析器
* @return 查询结果列表
*/
public <T> List<T> executeQuery(String sql, Function<Record, T> dataParser) {
try {
// 打印提示信息
log.info("开始执行ODPS数据查询...");
// 执行ODPS查询
Instance instance = SQLTask.run(tradeOdps, sql);
instance.waitForSuccess();
// 获取查询结果
List<Record> recordList = SQLTask.getResult(instance);
if (CollectionUtils.isEmpty(recordList)) {
log.info("完成执行ODPS数据查询: totalSize=0");
return Collections.emptyList();
}
// 依次读取数据
List<T> dataList = new ArrayList<>();
for (Record record : recordList) {
T data = dataParser.apply(record);
if (Objects.nonNull(data)) {
dataList.add(data);
}
}
// 打印提示信息
log.info("完成执行ODPS数据查询: totalSize={}", dataList.size());
// 返回查询结果
return dataList;
} catch (OdpsException e) {
log.warn("执行ODPS数据查询异常: sql={}", sql, e);
throw new BusinessException("执行ODPS数据查询异常", e);
}
}
}
5.2. 方法1:直接测试法(不推荐)
/**
* 交易订单服务测试类
*/
(MockitoJUnitRunner.class)
public class TradeOrderServiceTest {
/** 定义静态常量 */
/** 资源路径 */
private static final String RESOURCE_PATH = "testTradeOrderService/";
/** 模拟依赖对象 */
/** 交易ODPS服务 */
private TradeOdpsService tradeOdpsService;
/** 定义测试对象 */
/** 交易订单服务 */
private TradeOrderService tradeOrderService;
/**
* 测试: 查询交易订单-正常
*/
public void testQueryTradeOrderWithNormal() {
// 模拟依赖方法
// 模拟依赖方法: tradeOdpsService.executeQuery
List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class));
Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any());
// 调用测试方法
Long userId = 12345L;
Integer maxCount = 100;
Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount));
// 验证依赖方法
// 验证依赖方法: tradeOdpsService.executeQuery
String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any());
}
}
5.3. 方法2:联合测试法(不推荐)
/**
* 交易订单服务测试类
*/
public class TradeOrderServiceTest {
/** 定义静态常量 */
/** 资源路径 */
private static final String RESOURCE_PATH = "testTradeOrderService/";
/** 模拟依赖对象 */
/** 交易ODPS */
private Odps tradeOdps;
/** 定义测试对象 */
/** 交易ODPS服务 */
private TradeOdpsService tradeOdpsService = Mockito.spy(TradeOdpsService.class);
/** 交易订单服务 */
private TradeOrderService tradeOrderService;
/**
* 测试: 查询交易订单-正常
*
* @throws OdpsException ODPS异常
*/
public void testQueryTradeOrderWithNormal() throws OdpsException {
// 模拟依赖方法
PowerMockito.mockStatic(SQLTask.class);
// 模拟依赖方法: SQLTask.run
Instance instance = Mockito.mock(Instance.class);
PowerMockito.when(SQLTask.run(Mockito.eq(tradeOdps), Mockito.anyString())).thenReturn(instance);
// 模拟依赖方法: SQLTask.getResult
Record record1 = PowerMockito.mock(Record.class);
Record record2 = PowerMockito.mock(Record.class);
List<Record> recordList = Arrays.asList(record1, record2);
PowerMockito.when(SQLTask.getResult(instance)).thenReturn(recordList);
// 模拟依赖方法: record.getString
Mockito.doReturn(1L).when(record1).getBigint("id");
Mockito.doReturn(2L).when(record2).getBigint("id");
// 调用测试方法
Long userId = 12345L;
Integer maxCount = 100;
List<TradeOrderVO> tradeOrderList = tradeOrderService.queryTradeOrder(userId, maxCount);
String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderList.json");
Assert.assertEquals("交易订单列表不一致", text, JSON.toJSONString(tradeOrderList));
// 验证依赖方法
PowerMockito.verifyStatic(SQLTask.class);
// 验证依赖方法: SQLTask.run
text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
SQLTask.run(tradeOdps, text);
// 验证依赖方法: SQLTask.getResult
SQLTask.getResult(instance);
// 验证依赖方法: instance.waitForSuccess
Mockito.verify(instance).waitForSuccess();
// 验证依赖方法: record.getString
Mockito.verify(record1).getBigint("id");
Mockito.verify(record2).getBigint("id");
}
}
5.4. 方法3:重构测试法(推荐)
5.4.1. 重构代码
/**
* 交易订单服务类
*/
@Service
public class TradeOrderService {
/** 注入依赖对象 */
/** 交易ODPS服务 */
@Autowired
private TradeOdpsService tradeOdpsService;
/**
* 查询交易订单
*
* @param userId 用户标识
* @param maxCount 最大数量
* @return 交易订单列表
*/
public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) {
String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql");
String sql = String.format(format, userId, maxCount);
return tradeOdpsService.executeQuery(sql, TradeOrderService2::convertTradeOrder);
}
/**
* 转化交易订单
*
* @param record ODPS记录
* @return 交易订单
*/
private static TradeOrderVO convertTradeOrder(Record record) {
TradeOrderVO tradeOrder = new TradeOrderVO();
tradeOrder.setId(record.getBigint("id"));
// ...
return tradeOrder;
}
}
5.4.2. 测试用例
/**
* 交易订单服务测试类
*/
(MockitoJUnitRunner.class)
public class TradeOrderServiceTest {
/** 定义静态常量 */
/** 资源路径 */
private static final String RESOURCE_PATH = "testTradeOrderService/";
/** 模拟依赖对象 */
/** 交易ODPS服务 */
private TradeOdpsService tradeOdpsService;
/** 定义测试对象 */
/** 交易订单服务 */
private TradeOrderService tradeOrderService;
/**
* 测试: 查询交易订单-正常
*/
public void testQueryTradeOrderWithNormal() {
// 模拟依赖方法
// 模拟依赖方法: tradeOdpsService.executeQuery
List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class));
Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any());
// 调用测试方法
Long userId = 12345L;
Integer maxCount = 100;
Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount));
// 验证依赖方法
// 验证依赖方法: tradeOdpsService.executeQuery
String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql");
Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any());
}
/**
* 测试: 转化交易订单
*
* @throws Exception 异常信息
*/
public void testConvertTradeOrder() throws Exception {
// 模拟依赖方法
Long id = 12345L;
Record record = Mockito.mock(Record.class);
Mockito.doReturn(id).when(record).getBigint("id");
// 调用测试方法
TradeOrderVO tradeOrder = Whitebox.invokeMethod(TradeOrderService2.class, "convertTradeOrder", record);
Assert.assertEquals("订单标识不一致", id, tradeOrder.getId());
// 验证依赖方法
Mockito.verify(record).getBigint("id");
}
}
六、如何测试链式调用
6.1. 案例代码
/**
* 跨域辅助类
*/
public class CorsHelper {
/** 定义静态常量 */
/** 最大生命周期 */
private static final long MAX_AGE = 3600L;
/**
* 添加跨域支持
*
* @param registry 跨域注册器
* @return 跨域注册
*/
public static CorsRegistration addCorsMapping(CorsRegistry registry) {
return registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(MAX_AGE)
.allowedHeaders("*");
}
}
6.2. 方法1:普通测试法(不推荐)
/**
* 跨域辅助测试类
*/
public class CorsHelperTest {
/**
* 测试: 添加跨域支持
*/
public void testAddCorsMapping() {
// 模拟依赖方法
CorsRegistry registry = Mockito.mock(CorsRegistry.class);
CorsRegistration registration = Mockito.mock(CorsRegistration.class);
Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
Mockito.doReturn(registration).when(registration).allowedOrigins(Mockito.any());
Mockito.doReturn(registration).when(registration).allowedMethods(Mockito.any());
Mockito.doReturn(registration).when(registration).allowCredentials(Mockito.anyBoolean());
Mockito.doReturn(registration).when(registration).maxAge(Mockito.anyLong());
Mockito.doReturn(registration).when(registration).allowedHeaders(Mockito.any());
// 调用测试方法
Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));
// 验证依赖方法
Mockito.verify(registry).addMapping("/**");
Mockito.verify(registration).allowedOrigins("*");
Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
Mockito.verify(registration).allowCredentials(true);
Mockito.verify(registration).maxAge(3600L);
Mockito.verify(registration).allowedHeaders("*");
}
}
6.3. 方法2:利用RETURNS_DEEP_STUBS参数法(推荐)
/**
* 跨域辅助测试类
*/
public class CorsHelperTest {
/**
* 测试: 添加跨域支持
*/
@Test
public void testAddCorsMapping() {
// 模拟依赖方法
CorsRegistry registry = Mockito.mock(CorsRegistry.class, Answers.RETURNS_DEEP_STUBS);
CorsRegistration registration = Mockito.mock(CorsRegistration.class);
Mockito.when(registry.addMapping(Mockito.anyString())
.allowedOrigins(Mockito.any())
.allowedMethods(Mockito.any())
.allowCredentials(Mockito.anyBoolean())
.maxAge(Mockito.anyLong())
.allowedHeaders(Mockito.any()))
.thenReturn(registration);
// 调用测试方法
Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));
// 验证依赖方法
Mockito.verify(registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600L))
.allowedHeaders("*");
}
}
在mock对象时,需要指定Mockito.RETURNS_DEEP_STUBS参数;
在mock方法时,采用when-then模式,when内容是链式调用,then内容是返回的值;
在verify方法时,只需要验证最后1次方法调用,verify内容是前n次链式调用;如果验证时某个方法调用的某个参数指定错误时,最后一个方法调用验证将因为这个mock对象没有方法调用而抛出异常。
6.4. 方法3:利用RETURNS_SELF参数法(推荐)
/**
* 跨域辅助测试类
*/
public class CorsHelperTest {
/**
* 测试: 添加跨域支持
*/
@Test
public void testAddCorsMapping() {
// 模拟依赖方法
CorsRegistry registry = Mockito.mock(CorsRegistry.class);
CorsRegistration registration = Mockito.mock(CorsRegistration.class, Answers.RETURNS_SELF);
Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString());
// 调用测试方法
Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry));
// 验证依赖方法
Mockito.verify(registry).addMapping("/**");
Mockito.verify(registration).allowedOrigins("*");
Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS");
Mockito.verify(registration).allowCredentials(true);
Mockito.verify(registration).maxAge(3600L);
Mockito.verify(registration).allowedHeaders("*");
}
}
在mock对象时,对于自返回对象,需要指定Mockito.RETURNS_SELF参数;
在mock方法时,无需对自返回对象进行mock方法,因为框架已经mock方法返回了自身;
在verify方法时,可以像普通测试法一样优美地验证所有方法调用。
普通测试法:mock调用方法语句较多; 利用RETURNS_DEEP_STUBS参数法:mock调用方法语句较少,适合于链式调用返回不同值; 利用RETURNS_SELF参数法:mock调用方法语句最少,适合于链式调用返回相同值。
七、如何测试相同参数返回不同值
7.1. 案例代码
/**
* 读取数据
*
* @param <T> 模板类型
* @param recordReader 记录读取器
* @param dataParser 数据解析器
* @return 数据列表
* @throws IOException IO异常
*/
public static <T> List<T> readData(RecordReader recordReader, Function<Record, T> dataParser) throws IOException {
Record record;
List<T> dataList = new ArrayList<>();
while (Objects.nonNull(record = recordReader.read())) {
T data = dataParser.apply(record);
if (Objects.nonNull(data)) {
dataList.add(data);
}
}
return dataList;
}
7.2. 测试用例
/**
* 测试: 读取数据-正常
*
* @throws IOException IO异常
*/
public void testReadDataWithNormal() throws IOException {
// 模拟依赖方法
// 模拟依赖方法: recordReader.read
Record record1 = Mockito.mock(Record.class);
Record record2 = Mockito.mock(Record.class);
TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
Mockito.doReturn(record1, record2, null).when(recordReader).read();
// 模拟依赖方法: dataParser.apply
Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class));
Object object1 = new Object();
Object object2 = new Object();
Mockito.doReturn(object1).when(dataParser).apply(record1);
Mockito.doReturn(object2).when(dataParser).apply(record2);
// 调用测试方法
List<Object> dataList = OdpsHelper.readData(recordReader, dataParser);
Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataList);
// 验证依赖方法
// 验证依赖方法: recordReader.read
Mockito.verify(recordReader, Mockito.times(3)).read();
// 验证依赖方法: dataParser.apply
Mockito.verify(dataParser).apply(record1);
Mockito.verify(dataParser).apply(record2);
}
八、如何测试已变更的方法参数值
8.1. 案例代码
/**
* 读取数据
*
* @param <T> 模板类型
* @param recordReader 记录读取器
* @param batchSize 批量大小
* @param dataParser 数据解析器
* @param dataStorage 数据存储器
* @throws IOException IO异常
*/
public static <T> void readData(RecordReader recordReader, int batchSize,
Function<Record, T> dataParser, Consumer<List<T>> dataStorage) throws IOException {
// 依次读取数据
Record record;
List<T> dataList = new ArrayList<>(batchSize);
while (Objects.nonNull(record = recordReader.read())) {
// 解析添加数据
T data = dataParser.apply(record);
if (Objects.nonNull(data)) {
dataList.add(data);
}
// 批量存储数据
if (dataList.size() == batchSize) {
dataStorage.accept(dataList);
dataList.clear();
}
}
// 存储剩余数据
if (CollectionUtils.isNotEmpty(dataList)) {
dataStorage.accept(dataList);
dataList.clear();
}
}
8.2. 问题测试
/**
* 测试: 读取数据-正常
*
* @throws IOException IO异常
*/
public void testReadDataWithNormal() throws IOException {
// 模拟依赖方法
// 模拟依赖方法: recordReader.read
Record record1 = Mockito.mock(Record.class);
Record record2 = Mockito.mock(Record.class);
TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
Mockito.doReturn(record1, record2, null).when(recordReader).read();
// 模拟依赖方法: dataParser.apply
Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class));
Object object1 = new Object();
Object object2 = new Object();
Mockito.doReturn(object1).when(dataParser).apply(record1);
Mockito.doReturn(object2).when(dataParser).apply(record2);
// 调用测试方法
int batchSize = 2;
Consumer<List<Object>> dataStorage = CastUtils.cast(Mockito.mock(Consumer.class));
OdpsHelper.readData(recordReader, batchSize, dataParser, dataStorage);
// 验证依赖方法
// 验证依赖方法: recordReader.read
Mockito.verify(recordReader, Mockito.times(3)).read();
// 验证依赖方法: dataParser.apply
Mockito.verify(dataParser).apply(record1);
Mockito.verify(dataParser).apply(record2);
// 验证依赖方法: dataStorage.test
ArgumentCaptor<List<Object>> dataListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(dataStorage).accept(dataListCaptor.capture());
Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataListCaptor.getValue());
}
java.lang.AssertionError: 数据列表不一致 expected:<[java.lang.Object@7eaa2bc6, java.lang.Object@6dae70f9]> but was:<[]>
8.3. 正确测试
/**
* 测试: 读取数据-正常
*
* @throws IOException IO异常
*/
public void testReadDataWithNormal() throws IOException {
// 模拟依赖方法
// 模拟依赖方法: recordReader.read
Record record1 = Mockito.mock(Record.class);
Record record2 = Mockito.mock(Record.class);
TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class);
Mockito.doReturn(record1, record2, null).when(recordReader).read();
// 模拟依赖方法: dataParser.apply
Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class));
Object object1 = new Object();
Object object2 = new Object();
Mockito.doReturn(object1).when(dataParser).apply(record1);
Mockito.doReturn(object2).when(dataParser).apply(record2);
// 模拟依赖方法: dataStorage.test
List<Object> dataList = new ArrayList<>();
Consumer<List<Object>> dataStorage = CastUtils.cast(Mockito.mock(Consumer.class));
Mockito.doAnswer(invocation -> dataList.addAll(invocation.getArgument(0)))
.when(dataStorage).accept(Mockito.anyList());
// 调用测试方法
int batchSize = 2;
OdpsHelper.readData(recordReader, batchSize, dataParser, dataStorage);
Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataList);
// 验证依赖方法
// 验证依赖方法: recordReader.read
Mockito.verify(recordReader, Mockito.times(3)).read();
// 验证依赖方法: dataParser.apply
Mockito.verify(dataParser).apply(record1);
Mockito.verify(dataParser).apply(record2);
// 验证依赖方法: dataStorage.test
Mockito.verify(dataStorage).accept(Mockito.anyList());
}
九、如何测试相同返回值的代码分支
9.1. 案例代码
/**
* 灰度发布服务类
*/
@Slf4j
@Service
public class GrayReleaseService {
/** 定义静态常量 */
/** 灰度发布分子 */
private static final long GRAY_NUMERATOR = 0L;
/** 灰度发布分母 */
private static final long GRAY_DENOMINATOR = 10000L;
/** 注入依赖对象 */
/** 灰度发布配置 */
@Autowired
private GrayReleaseConfig grayReleaseConfig;
/**
* 是否灰度发布
*
* @param key 主键
* @param channel 渠道
* @param userId 用户标识
* @param value 取值
* @return 判断结果
*/
public boolean isGrayRelease(String key, String channel, String userId, Object value) {
// 判断灰度发布取值
if (Objects.isNull(value)) {
log.info("命中灰度取值为空");
return false;
}
// 获取灰度发布映射
Map<String, GrayReleaseItem> grayReleaseMap = grayReleaseConfig.getGrayReleaseMap();
if (MapUtils.isEmpty(grayReleaseMap)) {
log.info("命中灰度发布映射为空");
return false;
}
// 获取灰度发布项
GrayReleaseItem grayReleaseItem = grayReleaseMap.get(key);
if (Objects.isNull(grayReleaseItem)) {
log.info("命中灰度发布映项为空: key={}", key);
return false;
}
// 判断渠道白名单
Set<String> channelWhiteSet = grayReleaseItem.getChannelWhiteSet();
if (CollectionUtils.isNotEmpty(channelWhiteSet) && channelWhiteSet.contains(channel)) {
log.info("命中渠道白名单灰度: key={}, channel={}", key, channel);
return true;
}
// 判断用户白名单
Set<String> userIdWhiteSet = grayReleaseItem.getUserIdWhiteSet();
if (CollectionUtils.isNotEmpty(userIdWhiteSet) && userIdWhiteSet.contains(userId)) {
log.info("命中用户白名单灰度: key={}, userId={}", key, userId);
return true;
}
// 判断灰度发布比例
long grayNumerator = Optional.ofNullable(grayReleaseItem.getGrayNumerator()).orElse(GRAY_NUMERATOR);
long grayDenominator = Optional.ofNullable(grayReleaseItem.getGrayDenominator()).orElse(GRAY_DENOMINATOR);
boolean isGray = Math.abs(Objects.hashCode(value)) % grayDenominator <= grayNumerator;
log.info("命中灰度发布比例: key={}, value={}, isGray={}", key, value, isGray);
return isGray;
}
}
9.2. 普通测试法(不推荐)
/**
* 灰度发布服务测试类
*/
(MockitoJUnitRunner.class)
public class GrayReleaseServiceTest {
/** 模拟依赖方法 */
/** 灰度发布配置 */
private GrayReleaseConfig grayReleaseConfig;
/** 定义测试对象 */
/** 灰度发布服务 */
private GrayReleaseService grayReleaseService;
/**
* 测试: 是否灰度发布-命中渠道白名单
*/
public void testIsGrayReleaseWithChannelWhiteSet() {
// 模拟依赖方法
GrayReleaseItem grayReleaseItem = new GrayReleaseItem();
grayReleaseItem.setChannelWhiteSet(Sets.newHashSet("alipay"));
grayReleaseItem.setUserIdWhiteSet(Sets.newHashSet("123456"));
Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap();
grayReleaseMap.put("test", grayReleaseItem);
Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap();
// 调用测试方法
String key = "test";
String channel = "alipay";
String userId = "123456";
Object value = 1234567890L;
Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value));
// 验证依赖方法
Mockito.verify(grayReleaseConfig).getGrayReleaseMap();
}
}
9.3. 验证测试法(推荐)
/**
* 灰度发布服务测试类
*/
(MockitoJUnitRunner.class)
public class GrayReleaseServiceTest {
/** 模拟依赖方法 */
/** 灰度发布配置 */
private GrayReleaseConfig grayReleaseConfig;
/** 定义测试对象 */
/** 灰度发布服务 */
private GrayReleaseService grayReleaseService;
/**
* 测试: 是否灰度发布-命中渠道白名单
*/
public void testIsGrayReleaseWithChannelWhiteSet() {
// 模拟依赖方法
// 模拟依赖方法: grayReleaseItem.getChannelWhiteSet
GrayReleaseItem grayReleaseItem = Mockito.mock(GrayReleaseItem.class);
Mockito.doReturn(Sets.newHashSet("alipay")).when(grayReleaseItem).getChannelWhiteSet();
// 模拟依赖方法: grayReleaseItem.getUserIdWhiteSet
Mockito.doReturn(Sets.newHashSet("123456")).when(grayReleaseItem).getUserIdWhiteSet();
// 模拟依赖方法: grayReleaseConfig.getGrayReleaseMap
Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap();
grayReleaseMap.put("test", grayReleaseItem);
Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap();
// 调用测试方法
String key = "test";
String channel = "alipay";
String userId = "123456";
Object value = 1234567890L;
Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value));
// 验证依赖方法
// 验证依赖方法: grayReleaseConfig.getGrayReleaseMap
Mockito.verify(grayReleaseConfig).getGrayReleaseMap();
// 验证依赖方法: grayReleaseItem.getChannelWhiteSet
Mockito.verify(grayReleaseItem).getChannelWhiteSet();
// 验证依赖对象
Mockito.verifyNoMoreInteractions(grayReleaseConfig, grayReleaseItem);
}
}
Wanted but not invoked:
grayReleaseItem.getChannelWhiteSet();
9.4. 日志测试法(推荐)
/**
* 灰度发布服务测试类
*/
(MockitoJUnitRunner.class)
public class GrayReleaseServiceTest {
/** 模拟依赖方法 */
/** 日志器 */
private Logger log;
/** 灰度发布配置 */
private GrayReleaseConfig grayReleaseConfig;
/** 定义测试对象 */
/** 灰度发布服务 */
private GrayReleaseService grayReleaseService;
/**
* 在测试前
*/
public void beforeTest() {
FieldHelper.writeStaticFinalField(GrayReleaseService.class, "log", log);
}
/**
* 测试: 是否灰度发布-命中渠道白名单
*/
public void testIsGrayReleaseWithChannelWhiteSet() {
// 模拟依赖方法
GrayReleaseItem grayReleaseItem = new GrayReleaseItem();
grayReleaseItem.setChannelWhiteSet(Sets.newHashSet("alipay"));
grayReleaseItem.setUserIdWhiteSet(Sets.newHashSet("123456"));
Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap();
grayReleaseMap.put("test", grayReleaseItem);
Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap();
// 调用测试方法
String key = "test";
String channel = "alipay";
String userId = "123456";
Object value = 1234567890L;
Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value));
// 验证依赖方法
// 验证依赖方法: grayReleaseConfig.getGrayReleaseMap
Mockito.verify(grayReleaseConfig).getGrayReleaseMap();
// 验证依赖方法: log.info
Mockito.verify(log).info("命中渠道白名单灰度: key={}, channel={}", key, channel);
// 验证依赖对象
Mockito.verifyNoInteractions(log, grayReleaseConfig);
}
}
Argument(s) are different! Wanted:
log.info(
"命中渠道白名单灰度: key={}, channel={}",
"test",
"alipay"
);
-> at ...
Actual invocations have different arguments:
log.info(
"命中用户白名单灰度: key={}, userId={}",
"test",
"123456"
);
十、如何测试多线程并发编程
10.1. 案例代码
/**
* 交易订单服务类
*/
@Slf4j
@Service
public class TradeOrderService {
/** 定义静态常量 */
/** 等待时间(毫秒) */
private static final long WAIT_TIME = 1000L;
/** 注入依赖对象 */
/** 交易订单DAO */
@Autowired
private TradeOrderDAO tradeOrderDAO;
/** 执行器服务 */
@Autowired
private ExecutorService executorService;
/**
* 获取交易订单列表
*
* @param orderIdList 订单标识列表
* @return 交易订单列表
*/
public List<TradeOrderVO> getTradeOrders(List<Long> orderIdList) {
// 检查订单标识列表
if (CollectionUtils.isEmpty(orderIdList)) {
return Collections.emptyList();
}
// 获取交易订单期望
List<CompletableFuture<TradeOrderVO>> futureList = orderIdList.stream()
.map(this::getTradeOrder).collect(Collectors.toList());
// 聚合交易订单期望
CompletableFuture<List<TradeOrderVO>> joinFuture =
CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0]))
.thenApply(v -> futureList.stream().map(CompletableFuture::join).collect(Collectors.toList()));
// 返回交易订单列表
try {
return joinFuture.get(WAIT_TIME, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("获取订单中断异常", e);
throw new BusinessException("获取订单中断异常", e);
} catch (ExecutionException | TimeoutException | RuntimeException e) {
log.warn("获取订单其它异常", e);
throw new BusinessException("获取订单其它异常", e);
}
}
/**
* 获取交易订单
*
* @param orderId 订单标识
* @return 交易订单期望
*/
private CompletableFuture<TradeOrderVO> getTradeOrder(Long orderId) {
return CompletableFuture.supplyAsync(() -> tradeOrderDAO.get(orderId), executorService)
.thenApply(TradeOrderService::convertTradeOrder);
}
/**
* 转化交易订单
*
* @param tradeOrder 交易订单DO
* @return 交易订单VO
*/
private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) {
TradeOrderVO tradeOrderVO = new TradeOrderVO();
tradeOrderVO.setId(tradeOrder.getId());
// ...
return tradeOrderVO;
}
}
10.2. 测试用例
/**
* 交易订单服务测试类
*/
(MockitoJUnitRunner.class)
public class TradeOrderServiceTest {
/** 定义静态常量 */
/** 资源路径 */
private static final String RESOURCE_PATH = "testTradeOrderService/";
/** 模拟依赖对象 */
/** 交易订单DAO */
private TradeOrderDAO tradeOrderDAO;
/** 执行器服务 */
private ExecutorService executorService = Executors.newFixedThreadPool(10);
/** 定义测试对象 */
/** 交易订单服务 */
private TradeOrderService tradeOrderService;
/**
* 测试: 获取交易订单列表-正常
*/
public void testGetTradeOrdersWithNormal() {
// 模拟依赖方法
// 模拟依赖方法: tradeOrderDAO.get
String path = RESOURCE_PATH + "testGetTradeOrdersWithNormal/";
String text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderMap.json");
Map<Long, TradeOrderDO> tradeOrderMap = JSON.parseObject(text, new TypeReference<Map<Long, TradeOrderDO>>() {});
Mockito.doAnswer(invocation -> tradeOrderMap.get(invocation.getArgument(0)))
.when(tradeOrderDAO).get(Mockito.anyLong());
// 调用测试方法
text = ResourceHelper.getResourceAsString(getClass(), path + "orderIdList.json");
List<Long> orderIdList = JSON.parseArray(text, Long.class);
List<TradeOrderVO> tradeOrderList = tradeOrderService.getTradeOrders(orderIdList);
text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderList.json");
Assert.assertEquals("交易订单列表不一致", text, JSON.toJSONString(tradeOrderList));
// 验证依赖方法
// 验证依赖方法: tradeOrderDAO.get
ArgumentCaptor<Long> orderIdCaptor = ArgumentCaptor.forClass(Long.class);
Mockito.verify(tradeOrderDAO, Mockito.atLeastOnce()).get(orderIdCaptor.capture());
Assert.assertEquals("订单标识列表不一致", orderIdList, orderIdCaptor.getAllValues());
}
}
十一、附录
11.1. 引入Maven单测包
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
11.2. 使用到的工具方法
11.2.1. 以字符串方式获取资源
/**
* 资源辅助类
*/
public final class ResourceHelper {
/**
* 以字符串方式获取资源
*
* @param clazz 类
* @param name 资源名称
* @return 字符串
*/
public static <T> String getResourceAsString(Class<T> clazz, String name) {
try (InputStream is = clazz.getResourceAsStream(name)) {
return IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new IllegalArgumentException(String.format("以字符串方式获取资源(%s)异常", name), e);
}
}
}
11.2.2. 写入静态常量字段
/**
* 字段辅助类
*/
public final class FieldHelper {
/**
* 写入静态常量字段
*
* @param clazz 类
* @param fieldName 字段名称
* @param fieldValue 字段取值
*/
public static void writeStaticFinalField(Class<?> clazz, String fieldName, Object fieldValue) {
try {
Field field = clazz.getDeclaredField(fieldName);
FieldUtils.removeFinalModifier(field);
FieldUtils.writeStaticField(field, fieldValue, true);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new UnsupportedOperationException("写入静态常量字段异常", e);
}
}
}
后记
单元测试案例是无穷无尽的,如何系统化地呈现给读者是个大工程; 单元测试案例必须典型、合理、有意义,如何构建这些案例也很消耗精力。
《单测案例》 单元测试百家说,
案例总结方法多。
芳草满园花满目,
绿肥红瘦自斟酌。
往期系列文章:
那些年,我们写过的无效单元测试
Java编程技巧之单元测试用例简化方法(内含案例)
Java工程师必读手册
工匠追求“术”到极致,其实就是在寻“道”,且离悟“道”也就不远了,亦或是已经得道,这就是“工匠精神”——一种追求“以术得道”的精神。如果一个工匠只满足于“术”,不能追求“术”到极致去悟“道”,那只是一个靠“术”养家糊口的工匠而已。作者根据多年来的实践探索,总结了大量的Java编码之“术”,试图阐述出心中的Java编码之“道”。
点击阅读原文查看详情。
微信扫码关注该文公众号作者