《Java单元测试实战》——案例集锦:Java单元测试典型案例集锦(8) https://developer.aliyun.com/article/1232051?groupCode=java
九、 如何测试相同返回值的代码分支
在业务代码中,经常会出现不同的代码分支返回相同值的情况。这个时候,仅通过验证返回值是没法判断是否命中了对应的代码分支的。那么,这种情况如何进行单元测试呢?
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; } }
2. 普通测试法(不推荐)
这里,只测试了命中渠道白名单的情况。
/** * 灰度发布服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class GrayReleaseServiceTest { /** 模拟依赖方法 */ /** 灰度发布配置 */ @Mock private GrayReleaseConfig grayReleaseConfig; /** 定义测试对象 */ /** 灰度发布服务 */ @InjectMocks private GrayReleaseService grayReleaseService; /** * 测试: 是否灰度发布-命中渠道白名单 */ @Test 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(); } }
在一次代码重构中,把“判断用户白名单”放在“判断渠道白名单”之前,这个单元测试是无法检测出来的。
3. 验证测试法(推荐)
通过对mock方法的验证,可以相对准确地确定命中的代码分支。
/** * 灰度发布服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class GrayReleaseServiceTest { /** 模拟依赖方法 */ /** 灰度发布配置 */ @Mock private GrayReleaseConfig grayReleaseConfig; /** 定义测试对象 */ /** 灰度发布服务 */ @InjectMocks private GrayReleaseService grayReleaseService; /** * 测试: 是否灰度发布-命中渠道白名单 */ @Test 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); } }
如果把“判断用户白名单”放在“判断渠道白名单”之前,这个单元测试会报出以下错误日志:
错误日志告诉我们,grayReleaseItem.getChannelWhiteSet方法并没有被调用,所以不可能命中渠道白名单代码分支。
4. 日志测试法(推荐)
对于有日志打印的代码,可以通过验证日志方法来确定命中的代码分支,而且这种验证方法是非常简单直白的。如果没有日志打印,我们也可以添加日志打印(可能会涉及日志存储成本的增加)。
/** * 灰度发布服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class GrayReleaseServiceTest { /** 模拟依赖方法 */ /** 日志器 */ @Mock private Logger log; /** 灰度发布配置 */ @Mock private GrayReleaseConfig grayReleaseConfig; /** 定义测试对象 */ /** 灰度发布服务 */ @InjectMocks private GrayReleaseService grayReleaseService; /** * 在测试前 */ @Before public void beforeTest() { FieldHelper.writeStaticFinalField(GrayReleaseService.class, "log", log); } /** * 测试: 是否灰度发布-命中渠道白名单 */ @Test 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); } }
如果把“判断用户白名单”放在“判断渠道白名单”之前,这个单元测试会报出以下错误日志:
错误日志告诉我们,我们期望命中渠道白名单灰度代码分支,实际却命中的是用户白名单灰度代码分支。
《Java单元测试实战》——案例集锦:Java单元测试典型案例集锦(10) https://developer.aliyun.com/article/1232049?groupCode=java