为了更真实的展现单元测试的魅力,我使用目前工作中的项目一段代码,你不需要理解具体的业务,只需要了解如何如何写单元测试,以及感受单元测试的魅力就可以了。
这里我们改变一种方式,即我们先写功能代码,再补充单元测试,很多团队都是这样使用,虽然这样并不好,可是很多时候,我们新加入一个团队,不可能负责去做一个新的项目,都是在维护老的项目,并且当时的团队为了更快的编码而没有写单元测试。
简单了解,只需要注意 条件判断和外部依赖(调用其他类的方法),明白我们的单元测试代码需要覆盖到所有的条件判断,和隔离MOCK 外部依赖。
/** * 根据是否随机设置查询参数 * @param params * @param projectType * @param prjId * @return */ private Map<String, Object> setRandParam(Map<String, Object> params,Integer projectType,Integer prjId){ int allocateedAssetCountMin = (Integer)params.get("allocateedAssetCountMin");//资产池列表(前后台)最小项目已匹配资产条数 int assetRandRoundCount = (Integer)params.get("assetRandRoundCount");//资产池(前后台)随机资产-每次查询多少条 if (projectType != null && projectType.equals(ProjectType.LHB.getValue())) { prjId = LHB_PROJECTID; } boolean isRand = false;//是否使用随机债权,默认不使用 //1.根据prjId确认是否需要使用随机债权 if (prjId != null) { params.put("projectId", prjId); int investShareCount = investRepository.queryInvtShareCountByPrjId(prjId.toString()); int allocatedAssetCount = assetRepository.queryAllocatedAssetCountByPrjId(prjId.toString()); if (investShareCount == 0 || allocatedAssetCount < allocateedAssetCountMin) { int randAssetListCount = assetRepository.queryAssetPoolRandListCount(params);//随机债权数据总条数 isRand = true; Integer begin = getPoolRandListBegin(randAssetListCount, prjId, PRIME_NUMBER, assetRandRoundCount); Integer end = begin + assetRandRoundCount; params.put("begin", begin); params.put("end", end); } } MapUtil.addValueToMap(params, "isRand", isRand); return params; }
这段代码中,有条件判断,和依赖外部(数据库)。我们需要对这个做单元测试,要保证不受外界影响(数据库的结果)所以我们需要 MOCK(模拟外部调用返回的数据)
首先为了能够 运行单元测试我们需要引入 junit(我使用过的是Java,其他语言有其他的测试框架)
因为我们需要 @test 注解,和强大的断言 Assert .
<!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency>
为了mock 外部的依赖,我们需要使用mock 工具,这里使用 powerMock, 引入相关的 依赖包(maven搜索,这里不再展示)。
为了能够MOCK ,我们在测试类上面使用注解 @RunWith(PowerMockRunner.class) 代表这个类需要使用mock。
在类中对 测试的类使用 @injectMocks 注解我们测试的类, @mock 注解我们外部依赖的类。
@InjectMocks private AssetServiceFacade assetServiceFacade; @Mock private InvestRepository investRepository;
测试类的命名要求 类名+Test ,测试的方法 test+方法名,编写的测试用例上面 注解@Test ,junit就知道了这个方法是测试用例(毕竟测试类中还存在一些数据准备的方法)。
现在来看看,测试用代码:
根据功能代码中的条件判断,我们需要写三个用例 (这里被测试代码由于是私有的,编写比较特殊需要使用反射来获取方法,公共方法,直接调用类就可以了。)
##用例一:条件为NULL
@Test public void testSetRandParamWithNull() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{ //准备数据 Map<String, Object> params = Maps.newHashMap(); Integer prjId = null; Integer projectType = null; MapUtil.addValueToMap(params, "allocateedAssetCountMin", 500); MapUtil.addValueToMap(params, "assetRandRoundCount", 3000); //使用反射,调用方法 Class assetFacade = assetServiceFacade.getClass(); Method setRandParam = assetFacade.getDeclaredMethod("setRandParam",Map.class,Integer.class,Integer.class); setRandParam.setAccessible(true); params = (Map<String, Object>) setRandParam.invoke(assetServiceFacade, params,projectType,prjId); Assert.assertEquals(false, params.get("isRand")); }
用例二
@Test public void testSetRandParamWithLHB() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{ //准备数据 Map<String, Object> params = Maps.newHashMap(); Integer prjId = 3; Integer projectType = 104040; //灵活宝 MapUtil.addValueToMap(params, "allocateedAssetCountMin", 500); MapUtil.addValueToMap(params, "assetRandRoundCount", 3000); //使用反射,调用方法 Class assetFacade = assetServiceFacade.getClass(); Method setRandParam = assetFacade.getDeclaredMethod("setRandParam", Map.class,Integer.class,Integer.class); setRandParam.setAccessible(true); //mock when(investRepository.queryInvtShareCountByPrjId(anyString())).thenReturn((Integer)20); when(assetRepository.queryAllocatedAssetCountByPrjId(anyString())).thenReturn(600); params = (Map<String, Object>) setRandParam.invoke(assetServiceFacade, params,projectType,prjId); Assert.assertEquals(1, params.get("projectId")); Assert.assertEquals(false, params.get("isRand")); }
用例三
@Test public void testSetRandParamWithRand() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException{ //准备数据 Map<String, Object> params = Maps.newHashMap(); Integer prjId = 3; Integer projectType = 104040; MapUtil.addValueToMap(params, "allocateedAssetCountMin", 500); MapUtil.addValueToMap(params,"assetRandRoundCount", 3000); //使用反射,调用方法 Class assetFacade = assetServiceFacade.getClass(); Method setRandParam = assetFacade.getDeclaredMethod("setRandParam", Map.class,Integer.class,Integer.class); setRandParam.setAccessible(true); when(investRepository.queryInvtShareCountByPrjId(anyString())).thenReturn(20); when(assetRepository.queryAllocatedAssetCountByPrjId(anyString())).thenReturn(20); params = (Map<String, Object>) setRandParam.invoke(assetServiceFacade, params,projectType,prjId); Assert.assertEquals(true, params.get("isRand")); }
可以看到,上面三个的用例的大致步骤都是:
准备数据 (方法的参数)
mock相关的外部依赖,直接给出我们结果,when(调用方法).thenReturn(结果)。 当调用这个方法,直接返回 期望的结果。
断言判断关键结果是否符合我们的期望。(如果不符合,则修改功能代码,前提保证测试的逻辑是没有问题的。)
这个时候在单元测试右键 Run as -> Junit Test ,可以看到结果,我的这个,我已经运行成功,但是不认为这是一次就运行成功。事情都是曲折发展,在你编写单元测试的过程中(我们这种方式先编码,再补单元测试)会经常 修改单元测试代码保证单元测试代码没有问题,如果之后测试还跑不通,那么这个时候大胆的修改代码,因为你有单元测试这个强大的工具保驾护航。
测试完成之后,你还需要保证,你的单元测试代码的没有忘记哪个分支条件,此时使用 检测覆盖率工具 EclEmma
进行覆盖的检查。
安装完 EclEmma ,插件右键 coverage as —> junit test ,可以看到我们的功能代码,有了颜色标记,绿色代表 测试代码覆盖到了,红色代表没有覆盖到,黄色代表没有完全覆盖。
那么现在我们总结一下,如果编写单元测试代码:
准备 junit 测试框架
创建测试类
引入 mock工具,mock外部依赖,我们这里使用 powerMock,你可以选择其他的。
检测单元测试覆盖率
作者:我爱看明朝
原文链接:https://blog.csdn.net/u013565163/article/details/77017286