定义:单元测试(unit testing)是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作,在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
优缺点: 单元测试从长期来看,可以提高代码质量,减少维护成本,减少调试时间,降低重构难度。但是从短期来看,加大了工作量,对于进度紧张的项目中的开发人员来说,可能会成为不少的负担。
写单元测试的时机不外乎三种情况:
1、在具体实现代码之前。这是测试驱动开发(TDD)所提倡的;
2、与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。
3、编写完功能代码再写单元测试。事后编写的单元测试“粒度”都比较粗。
建议:推荐单元测试与具体实现代码同步进行这个方案的。只有对需求有一定的理解后才能知道什么是代码的正确性,才能写出有效的单元测试来验证正确性,而能写出一些功能代码则说明对需求有一定理解了。
代码覆盖率 = 代码的覆盖程度,一种度量方式。
在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或 90%。
又称行覆盖(LineCoverage),段覆盖(SegmentCoverage),基本块覆盖(BasicBlockCoverage),这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了。它只管覆盖代码中的执行语句,却不考虑各种分支的组合等等。但是,换来的测试效果不明显,很难更多地发现代码中的问题。举个简单的例子:
int divide(int a, int b){
return a / b;
}
TeseCase: a = 10, b = 5 。测试结果会告诉你,代码覆盖率达到了100%,并且所有测试案例都通过了。然而遗憾的是,我们的语句覆盖率达到了所谓的100%,但是却没有发现最简单的Bug,比如,当我让b=0时,会抛出一个除零异常。
又称分支覆盖(BranchCoverage),所有边界覆盖(All-EdgesCoverage),基本路径覆盖(BasicPathCoverage),判定路径覆盖(Decision-Decision-Path)。它度量程序中每一个判定的分支是否都被测试到了。
3.条件覆盖(ConditionCoverage)
它度量判定中的每个子表达式结果true和false是否被测试到了。
判定覆盖和条件覆盖的区别举例:
int foo(int a, int b){
if (a < 10 || b < 10){ // 判定
return 0; // 分支一
}else{
return 1; // 分支二
}
}
设计判定覆盖案例时,我们只需要考虑判定结果为true和false两种情况,因此,我们设计如下的案例就能达到判定覆盖率100%:
TestCaes1: a = 5, b = 任意数字 //覆盖了分支一
TestCaes2: a = 15, b = 15 //覆盖了分支二
设计条件覆盖案例时,我们需要考虑判定中的每个条件表达式结果,为了覆盖率达到100%,我们设计了如下的案例:(全部为true和全部为false)
TestCase1: a = 5, b = 5 true, true
TestCase2: a = 15, b = 15 false, false
需要特别注意的是:条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到了就OK了。因此,我们可以这样推论:完全的条件覆盖并不能保证完全的判定覆盖。比如上面的例子,假如我设计的案例为:
TestCase1: a = 5, b = 15 true, false 分支一
TestCase1: a = 15, b = 5 false, true 分支一
可以看到,虽然我们完整的做到了条件覆盖,但是我们却没有做到完整的判定覆盖,我们只覆盖了分支一。
又称断言覆盖(PredicateCoverage)。它度量了是否函数的每一个分支都被执行了。就是所有可能的分支都执行一遍,有多个分支嵌套时,需要对多个分支进行排列组合,可想而知,测试路径随着分支的数量指数级别增加。
int foo(int a, int b){
int nReturn = 0;
if (a < 10){ // 分支一
nReturn += 1;
}
if (b < 10){ // 分支二
nReturn += 10;
}
return nReturn;
}
a. 语句覆盖 语句覆盖率100%
TestCase a = 5, b = 5 nReturn = 11
b. 判定覆盖 判定覆盖率100%
TestCase1 a = 5, b = 5 nReturn = 11
TestCase2 a = 15, b = 15 nReturn = 0
c. 条件覆盖 条件覆盖率100%
TestCase1 a = 5, b = 15 nReturn = 1 true fasle
TestCase2 a = 15, b = 5 nReturn = 10 false true
上面三种覆盖率结果看起来都都达到了100%!但是,nReturn的结果一共有四种可能的返回值:0,1,10,11,而我们上面的针对每种覆盖率设计的测试案例只覆盖了部分返回值,并没有测试完全。
d.路径覆盖 将所有可能的返回值都测试到了
TestCase1 a = 5, b = 5 nReturn = 0
TestCase2 a = 15, b = 5 nReturn = 1
TestCase3 a = 5, b = 15 nReturn = 10
TestCase4 a = 15, b = 15 nReturn = 11
其他代码覆盖率的指标还有函数覆盖率(function coverage);调用覆盖率(call coverage);循环覆盖率(loop coverage)等等,不同的公司对不同的覆盖率有不同的要求。
注意:
idea中支持三种插件来查看代码覆盖率,每种插件统计明细各有千秋,分别是idea自带插件Coverage、JaCoCo、Emma。目前市场上主要代码覆盖率工具有:
Emma 通过对编译后的 Java 字节码文件进行插桩,在测试执行过程中收集覆盖率信息,并通过支持多
种报表格式对覆盖率结果进行展示。
Cobertura 配置内容很丰富
Jacoco Jacoco 也是 Emma 团队开发,可以认为是Emma的升级版
Clover(商用) 最早的JAVA测试代码覆盖率工具之一,收费
工具 | Jacoco | Emma | Cobertura |
---|---|---|---|
原理 | 使用 ASM(致力于字节码操作和分析的框架,它可用来修改一个已存在的类或者动态产生一个新的类)修改字节码 | 修改 jar 文件,class 文件字节码文件 | 基于 jcoverage,基于 asm 框架对 class 文件插桩 |
覆盖粒度 | 行,类,方法,指令,分支 | 行,类,方法,基本块,指令,无分支覆盖 | 项目,包,类,方法的语句覆盖/分支覆盖 |
生成结果 | html、csv、xml | html、xml、txt,二进制格式报表 | html,xml |
缺点 | 需要源代码 | 1、需要 debug 版本,并打来 build.xml 中的 debug 编译项; 2、需要源代码,且必须与插桩的代码完全一致 | 1、不能捕获测试用例中未考虑的异常; 2、关闭服务器才能输出覆盖率信息(已有修改源代码的解决方案,定时输出结果; |
性能 | 快 | 小巧 | 插入的字节码信息更多 |
执行方式 | maven,ant,命令行 | 命令行 | maven,ant |
Jenkins 集成 | 生成 html 报告,直接与 hudson 集成,展示报告,无趋势图 | 无法与 hudson 集成 | 有集成的插件,美观的报告,有趋势图 |
报告实时性 | 默认关闭,可以动态从 jvm dump 出数据 | 可以不关闭服务器 | 默认是在关闭服务器时才写结果 |
维护状态 | 持续更新中 | 停止维护 | 停止维护 |
注:Hudson是Jenkins的前身,是基于Java开发的一种持续集成工具,用于监控程序重复的工作。
行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
类覆盖率:度量计算class类文件是否被执行。
分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的 分支数量。
方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。
指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全独立源码格式。
圈复杂度:代码复杂度的衡量标准,在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测试案例没有完全覆盖到这个模块。 简单来说就是覆盖所有的可能情况最少使用的测试用例数。
(1) JaCoCo在Byte Code时使用的ASM技术修改字节码方法,可以修改Jar文件、class文件字节码文件。
(2) JaCoCo通过注入来修改和生成java字节码,使用的是ASM库。
(3) JaCoCo同时支持on-the-fly(及时)和offline(离线)的两种插桩模式。
On-the-fly插桩:
JVM通过-javaagent参数指定特定的jar文件启动Instrumentation代理程序,代理程序在装载class文件前判断是否已经转换修改了该文件,若没有则将探针(统计代码)插入class文件,最后在JVM执行测试代码的过程中完成对覆盖率的分析。
Offline模式:
先对字节码文件进行插桩,然后执行插桩后的字节码文件,生成覆盖信息并导出报告。
On-the-fly和offline比较:
On-the-fly无需提前进行字节码插桩,无需停机(offline需要停机),可以实时获取覆盖率。
存在如下情况不适合on-the-fly,需要采用offline提前对字节码插桩:
(1) 运行环境不支持java agent。
(2) 部署环境不允许设置JVM参数。
(3) 字节码需要被转换成其他的虚拟机如Android Dalvik VM。
(4) 动态修改字节码过程中和其他agent冲突。
(5) 无法自定义用户加载类。
Jacoco支持Apache Ant、命令行、Apache Maven方式使用,这里只对在IDEA中Maven使用说明。
1 . Maven配置
(1)pom.xml文件中添加依赖
org.jacoco
jacoco-maven-plugin
0.8.2
(2)配置plugins插件信息(比较详细的配置,包括覆盖率和缺失率的详细设置)
org.jacoco
jacoco-maven-plugin
0.8.2
target/coverage-reports/jacoco-unit.exec
target/coverage-reports/jacoco-unit.exec
**/service/**
**/controller/**
BUNDLE
METHOD
COVEREDRATIO
0.50
BRANCH
COVEREDRATIO
0.50
CLASS
MISSEDCOUNT
0
jacoco-initialize
prepare-agent
check
check
jacoco-site
package
report
在rule中配置的规则,有的是 COVEREDRATIO,有的是MISSEDCOUNT,这说明有的统计的是覆盖率,有的统计的是丢失率(也即未覆盖到的)。
我们可以在pom文件的plugin里面配置rule规则和check 目标,所以在覆盖率不满足的情况下,mvn install是不会成功的,并且会报错。
上面配置主要做了一下的构建过程:
(1) 项目已jar包方式打包,引入junit和jacoco。
(2) Build时执行instrument、report、check。
(3) 覆盖率生成到target/jacoco.exec
当JaCoCo插件配置好以后,要获得 JaCoCo的统计数据,就要执行mvn install 命令。执行完以后,target/site/jacoco/目录下会生成一个index.html文件,这是统计数据总览页面,可以在浏览器打开查看。
注:如果没生成site目录,则需要手动通过Jacoco插件点击jacoco:report生成index.html
1、 报告文档分析
Jacoco 包含了多种尺度的覆盖率计数器,包含指令级(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈复杂度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、类(Classes)。
Instructions: Jacoco 计算的最小单位就是字节码指令。指令覆盖率表明了在所有的指令中,哪些被执行过以及哪些没有被执行。这项指数完全独立于源码格式并且在任何情况下有效,不需要类文件的调试信息。
Branches: Jacoco 对所有的 if 和 switch 指令计算了分支覆盖率。这项指标会统计所有的分支数量,并同时支出哪些分支被执行,哪些分支没有被执行。这项指标也在任何情况都有效。异常处理不考虑在分支范围内。
Cyclomatic Complexity: Jacoco 为每个非抽象方法计算圈复杂度,并也会计算每个类、包、组的复杂度。根据 McCabe 1996 的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。这项参数也在任何情况下有效。
Lines: 该项指数在有调试信息的情况下计算。
Methods: 每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为 Jacoco 直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。
Classes:每个类中只要有一个方法被执行,这个类就被认定为被执行。同 Methods 一样,有些没有在源码声明的方法被执行,也认定该类被执行。
2、详细文档分析
如下所示,标示绿色的为分支覆盖充分,标黄色的为部分分支覆盖,标红色的为未执行该分支。
在有调试信息的情况下,分支点可以被映射到源码中的每一行,并且被高亮表示。
红色钻石:无覆盖,没有分支被执行。
黄色钻石:部分覆盖,部分分支被执行。
绿色钻石:全覆盖,所有分支被执行。
通过这个报告的结果就可以知道代码真实的执行情况,便于我们分析评估结果,并且可以提高代码的测试覆盖率。
在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。Mockito是mocking框架,它让你用简洁的API做测试。
简单来说,所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象。Mockito 的优点是通过在执行后校验哪些函数已经被调用,消除了对期望行为(expectations)的需要。其它的 mocking 库需要在执行前记录期望行为(expectations),而这导致了丑陋的初始化代码。
Mockito区别于其他模拟框架的地方主要是允许开发者在没有建立“预期”时验证被测系统的行为。
1、添加Maven依赖 (同时也需要导入junit包依赖)
org.mockito
mockito-all
1.9.5
test
junit
junit
4.11
test
官方文档地址:http://static.javadoc.io/org.mockito/mockito-core/2.23.4/org/mockito/Mockito.html
2、添加引用
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
3、常用Mockito模拟方法
方法名 | 描述 |
---|---|
Mockito.mock(classToMock) | 模拟对象 |
Mockito.verify(mock) | 验证行为是否发生 |
Mockito.when(methodCall).thenReturn(value1).thenReturn(value2) | 发时第一次返回value1,第n次都返回value2 |
Mockito.doThrow(toBeThrown).when(mock).[method] | 模拟抛出异常 |
Mockito.mock(classToMock,defaultAnswer) | 使用默认Answer模拟对象 |
Mockito.when(methodCall).thenReturn(value) | 参数匹配,执行方法得到返回值value |
Mockito.doReturn(toBeReturned).when(mock).[method] | 参数匹配(直接执行不判断) |
Mockito.when(methodCall).thenAnswer(answer)) | 预期回调接口生成期望值 |
Mockito.doAnswer(answer).when(methodCall).[method] | 预期回调接口生成期望值(直接执行不判断) |
Mockito.spy(Object) | 用spy监控真实对象,设置真实对象行为 |
Mockito.doNothing().when(mock).[method] | 不做任何返回 |
Mockito.doCallRealMethod().when(mock).[method] //等价于Mockito.when(mock.[method]).thenCallRealMethod(); | 调用真实的方法 |
reset(mock) | 重置mock |
1、验证行为是否发生
//模拟创建一个List对象
List mock = Mockito.mock(List.class);
//调用mock对象的方法
mock.add(1);
mock.clear();
//验证方法是否执行
Mockito.verify(mock).add(1);
Mockito.verify(mock).clear();
验证是否发生add(0)和clear()这两种方法,如果和验证的不一致,则测试不通过。
2、多次触发返回不同值
//mock一个Iterator类
Iterator iterator = mock(Iterator.class);
//预设当iterator调用next()时第一次返回hello,第n次都返回world
Mockito.when(iterator.next()).thenReturn("hello").thenReturn("world");
//使用mock的对象
String result = iterator.next() + " " + iterator.next() + " " + iterator.next();
//验证结果
Assert.assertEquals("hello world world",result);
3、模拟抛出异常
@Test(expected = IOException.class) //期望报Io异常
public void when_thenThrow() throws IOException{
OutputStream mock = Mockito.mock(OutputStream.class);
//预设当流关闭时抛出异常
Mockito.doThrow(new IOException()).when(mock).close();
mock.close();
}
4、参数匹配
@Test
public void with_arguments(){
B b = Mockito.mock(B.class);
//预设根据不同的参数返回不同的结果
Mockito.when(b.getSex(1)).thenReturn("男");
Mockito.when(b.getSex(2)).thenReturn("女");
Assert.assertEquals("男", b.getSex(1));
Assert.assertEquals("女", b.getSex(2));
//对于没有预设的情况会返回默认值
Assert.assertEquals(null, b.getSex(0));
}
5、匹配任意参数
Mockito.anyInt() 任何int值 、Mockito.anyLong() 任何long值 、Mockito.anyString() 任何String值
@Test
public void with_unspecified_arguments(){
List list = Mockito.mock(List.class);
//匹配任意参数
Mockito.when(list.get(Mockito.anyInt())).thenReturn(1);
Mockito.when(list.contains(Mockito.argThat(new IsValid()))).thenReturn(true);
Assert.assertEquals(1,list.get(1));
Assert.assertEquals(1,list.get(999));
Assert.assertTrue(list.contains(1));
Assert.assertTrue(!list.contains(3));
}
class IsValid extends ArgumentMatcher{
@Override
public boolean matches(Object obj) {
return obj.equals(1) || obj.equals(2);
}
}
6、匹配自定义参数
@Test
public void argumentMatchersTest(){
//创建mock对象
List mock = mock(List.class);
//argThat(Matches matcher)方法用来应用自定义的规则,可以传入任何实现Matcher接口的实现类。
Mockito.when(mock.addAll(Mockito.argThat(new IsListofTwoElements()))).thenReturn(true);
Assert.assertTrue(mock.addAll(Arrays.asList("one","two","three")));
}
class IsListofTwoElements extends ArgumentMatcher{
public boolean matches(Object list){
return((List)list).size()==3;
}
}
7、用spy监控真实对象,设置真实对象行为
@Test(expected = IndexOutOfBoundsException.class)
public void spy_on_real_objects(){
List list = new LinkedList();
List spy = Mockito.spy(list);
//下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
//Mockito.when(spy.get(0)).thenReturn(3);
//使用doReturn-when可以避免when-thenReturn调用真实对象api
Mockito.doReturn(999).when(spy).get(999);
//预设size()期望值
Mockito.when(spy.size()).thenReturn(100);
//调用真实对象的api
spy.add(1);
spy.add(2);
Assert.assertEquals(100,spy.size());
Assert.assertEquals(1,spy.get(0));
Assert.assertEquals(2,spy.get(1));
Assert.assertEquals(999,spy.get(999));
}
8、调用真实的方法
@Test
public void Test() {
A a = Mockito.mock(A.class);
//void 方法才能调用doNothing()
Mockito.when(a.getName()).thenReturn("bb");
Assert.assertEquals("bb",a.getName());
//等价于Mockito.when(a.getName()).thenCallRealMethod();
Mockito.doCallRealMethod().when(a).getName();
Assert.assertEquals("zhangsan",a.getName());
}
class A {
public String getName(){
return "zhangsan";
}
}
9、重置mock
@Test
public void reset_mock(){
List list = mock(List.class);
Mockito. when(list.size()).thenReturn(10);
list.add(1);
Assert.assertEquals(10,list.size());
//重置mock,清除所有的互动和预设
Mockito.reset(list);
Assert.assertEquals(0,list.size());
}
它的API接口还有很多调用方法…
1、使用@Mock注释
public class MockitoTest {
@Mock
private List mockList;
//必须在基类中添加初始化mock的代码,否则报错mock的对象为NULL
public MockitoTest(){
MockitoAnnotations.initMocks(this);
}
@Test
public void AnnoTest() {
mockList.add(1);
Mockito.verify(mockList).add(1);
}
}
2、指定测试类使用运行器:MockitoJUnitRunner
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest2 {
@Mock
private List mockList;
@Test
public void shorthand(){
mockList.add(1);
Mockito.verify(mockList).add(1);
}
}
MockMvc为spring测试下的一个非常好用的类,配合Mockito使用能达到非常好的效果,他们的初始化需要在setUp中进行。
// mock api 模拟http请求
private MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
// 初始化工作
@Before
public void setUp() {
// 集成Web环境测试(此种方式并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@After
public void tearDown()throws Exception {
// Add additional tear down code here
}
mockMvc.perform( MockMvcRequestBuilders.post("/parkingsearch")
.accept(MediaType.APPLICATION_JSON_UTF8).content(new String(content.getBytes("GB2312"), "utf-8"))
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.jsonPath("$.resCode").value(0))
.andExpect(MockMvcResultMatchers.jsonPath("$.resData.data").isArray())
.andExpect(MockMvcResultMatchers.status().isOk());
perform:执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理;andExpect:添加ResultMatcher验证规则,验证控制器执行完成后结果是否正确;
andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台;
andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理;
MockMvc是基于RESTful风格的SpringMVC的测试框架,可以测试完整的Spring MVC流程,即从URL请求到控制器处理,再到视图渲染都可以测试。
集成Web环境方式
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SearchServiceApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class IncotermsRestServiceTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); //构造MockMvc
}
...
}