上一篇中介绍了,何为测试驱动,为什么需要测试驱动? 现在我们来看看怎样编写单元测试。
为了更真实的展现单元测试的魅力,我使用目前工作中的项目一段代码(因为我太懒得在写一段代码。。。。),你不需要理解具体的业务,只需要了解如何如何写单元测试,以及感受单元测试的魅力就可以了。
这里我们改变一种方式,即我们先写功能代码,再补充单元测试,很多团队都是这样使用,虽然这样并不好,可是很多时候,我们新加入一个团队,不可能负责去做一个新的项目,都是在维护老的项目,并且当时的团队为了更快的编码而没有写单元测试(这样并不能提高效率,但是人们为了短期的利益,而这样的事情在我们生活中处处可见。)
简单了解,只需要注意 条件判断和外部依赖(调用其他类的方法),明白我们的单元测试代码需要覆盖到所有的条件判断,和隔离MOCK 外部依赖。
/**
* 根据是否随机设置查询参数
* @param params
* @param projectType
* @param prjId
* @return
*/
private Map setRandParam(Map 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 .
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
为了mock 外部的依赖,我们需要使用mock 工具,这里使用 powerMock, 引入相关的 依赖包(maven搜索,这里不再展示)。
为了能够MOCK ,我们在测试类上面使用注解 @RunWith(PowerMockRunner.class) 代表这个类需要使用mock。
在类中对 测试的类使用 @injectMocks 注解我们测试的类, @mock 注解我们外部依赖的类。
@InjectMocks
private AssetServiceFacade assetServiceFacade;
@Mock
private InvestRepository investRepository;
测试类的命名要求 类名+Test ,测试的方法 test+方法名,编写的测试用例上面 注解@Test ,junit就知道了这个方法是测试用例(毕竟测试类中还存在一些数据准备的方法)。
现在来看看,测试用代码:
根据功能代码中的条件判断,我们需要写三个用例 (这里被测试代码由于是私有的,编写比较特殊需要使用反射来获取方法,公共方法,直接调用类就可以了。)
@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"));
}
可以看到,上面三个的用例的大致步骤都是:
1.准备数据 (方法的参数)
2.mock相关的外部依赖,直接给出我们结果,when(调用方法).thenReturn(结果)。 当调用这个方法,直接返回 期望的结果。
3.断言判断关键结果是否符合我们的期望。(如果不符合,则修改功能代码,前提保证测试的逻辑是没有问题的。)
这个时候在单元测试右键 Run as -> Junit Test ,可以看到结果,我的这个,我已经运行成功,但是不认为这是一次就运行成功。事情都是曲折发展,在你编写单元测试的过程中(我们这种方式先编码,再补单元测试)会经常 修改单元测试代码保证单元测试代码没有问题,如果之后测试还跑不通,那么这个时候大胆的修改代码,因为你有单元测试这个强大的工具保驾护航。
测试完成之后,你还需要保证,你的单元测试代码的没有忘记哪个分支条件,此时使用 检测覆盖率工具 EclEmma
进行覆盖的检查。
安装完 EclEmma ,插件右键 coverage as —> junit test ,可以看到我们的功能代码,有了颜色标记,绿色代表 测试代码覆盖到了,红色代表没有覆盖到,黄色代表没有完全覆盖。
那么现在我们总结一下,如果编写单元测试代码:
关注我的公众号第一时间阅读有趣的技术故事
扫码关注:
也可以在微信搜索公众号即可关注我:codexiulian
渴望与你一起成长进步!