1 单元测试与Junit4测试框架:
单元测试:是指对软件中的最小可测试单元进行检查和验证。在java应用程序中常常指的是一个方法(但并不总是如此)。
Junit4测试框架:框架是一个应用程序的半成品。框架提供了可在应用程序之间共享的课服用的公共结构。例如下面在Eclipse中运行Junit4生成的框架:
package com.huawei.osm.incident.utils;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class MybatisUtilsTest
{
@BeforeClass
public static void setUpBeforeClass() throws Exception
{
}
@AfterClass
public static void tearDownAfterClass() throws Exception
{
}
@Before
public void setUp() throws Exception
{
}
@After
public void tearDown() throws Exception
{
}
@Test
public void testGetSqlSession()
{
fail("Not yet implemented");
}
@Test
public void testCloseSqlSession()
{
fail("Not yet implemented");
}
}
2 了解Junit4新特性:
2.1 Junit4新特性
1. 必须引入的Test类:import org.junit.Test。旧的Junit3是需要继承Test。以及新的类:org.junit.Assert.*。
2. 采用@Befor和@After加载或者清除资源。相当于许多方法类中的init()和destroy();
3. 属于类范围的 setUp()方法 和 tearDown() 方法,任何用 @BeforeClass 注释的方法都将在该类中的测试方法运行之前运行一次,而任何用 @AfterClass 注释的方法都将在该类中的所有测试都运行之后运行一次;
4. 提供@Test(expected=“可能抛出的异常”)对异常测试的注解
5. 提供@lgnore注解,意即被@Ignore标记的方法会在运行测试时跳过,或者说被改元素标记的方法在测试中会被忽略。当测试的方法还没有实现,或者测试的方法已经过时,或者在某种条件下才能测试该方法(比如需要一个数据库联接,而在本地测试的时候,数据库并没有连接),那么使用该标签来标示这个方法。同时,你可以为该标签传递一个String的参数,来表明为什么会忽略这个测试方法。比如:@lgnore(“该方法还没有实现”),在执行的时候,仅会报告该方法没有实现,而不会运行测试方法。
6. 提供测试响应时间@Test(timeout=1000)(单位:毫秒),如果测试的运行时间超过指定的毫秒数,即认为测试失败。
7. 增加两个断言方法:
(1)public static void assertEquals(Object[] expected, Object[] actual)
(2)public static void assertEquals(String message, Object[] expected, Object[] actual) 这两用来比较数组:如果数组的长度和对应的元素相同,测这两个数组相等,否则不等,也考虑了数组为空的情况。
2.2 Junit4中常见的几个annotation释义
@Test:测试方法,可以测试期望异常和超市时间
@Before:初始化方法,每次执行@Test之前都会运行一次。
@After:资源释放,每次执行@Test之后都会运行一次。
@Ignore:忽略的测试方法
@BeforeClass、@AfterClass:注意区分@Before,它是针对所有的测试,在所在类中只会执行一次,只能定义为static void。同理可以接@AfterClass。
2.3 Junit4的单元测试用例执行顺序为
@BeforeClass>>@Before>>@Test>>@After>>@AfterClass.
3 Junit4测试案例
3.1 Eclipse中使用Junit4
下面是我们在Eclipse中新建的一个用来做测试的一个计算器类Claculator
package com.huawei.demo;
public class Calculator
{
private static int result;
public void add(int n)
{
result = result + n;
}
public void subs(int n)
{
result = result - n;
}
// 此方法尚未写好
public void multiply(int n)
{
// 此方法尚未写好
}
public void divide(int n)
{
result = result / n;
}
public void square(int n)
{
result = n * n;
}
//Bug : 死循环
public void squareRoot(int n)
{
for (; ;) ;
}
// 将结果清零
public void clear()
{
result = 0;
}
public int getResult()
{
return result;
}
}
第二步,在我们的项目中引入JUnit4的jar包(略)
第三步,右键点击我们刚才新建的Claculator类,选择“New”新建一个“JUnit Test Case”,如图:
点击“下一步”,进入勾选测试方法的页面,根据情况选择需要做测试的单元,如图:
点击“完成”。Eclipse会自动帮我们生成Junit4的测试框架,如下:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
public class CalculatorTest
{
@Before
public void setUp() throws Exception
{
}
@Test
public void testAdd()
{
fail("Not yet implemented");
}
@Test
public void testSubs()
{
fail("Not yet implemented");
}
@Test
public void testMultiply()
{
fail("Not yet implemented");
}
@Test
public void testDivide()
{
fail("Not yet implemented");
}
@Test
public void testSquare()
{
fail("Not yet implemented");
}
@Test
public void testSquareRoot()
{
fail("Not yet implemented");
}
}
第四步:编写我们的测试内容。(在Calculator类中预留的bug和exception都得处理好。)修改后如下:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import com.huawei.demo.Calculator;
public class CalculatorTest
{
private static Calculator calculator = new Calculator();
@Before
public void setUp() throws Exception
{
calculator.clear();
}
@Test
public void testAdd()
{
calculator.add(2);
calculator.add(3);
calculator.add(-5);
assertEquals(0, calculator.getResult());
}
@Test
public void testSubs()
{
calculator.add(10);
calculator.subs(2);
assertEquals(8, calculator.getResult());
}
@Ignore("Multiply() Not yet implemented")
@Test
public void testMultiply()
{
}
@Test
public void testDivide()
{
calculator.add(16);
calculator.divide(-2);
calculator.divide(-2);
assertEquals(4, calculator.getResult());
}
@Test(expected = ArithmeticException.class)
public void divideByZero()
{
calculator.divide(0);
}
@Test
public void testSquare()
{
calculator.square(-2);
calculator.square(4);
assertEquals(16, calculator.getResult());
}
@Test(timeout = 1000)
public void testSquareRoot()
{
calculator.squareRoot(4);
assertEquals(2, calculator.getResult());
}
}
如下:运行改单元测试案例,可以查看测试结果:
3.2 Gradle中集成Junit4单元测试
第一步:按照gradle项目目录结构新建一个“gradle-junit”的项目工程,该工程下包含标示gradle项目的build.gradle文件。并将我们刚才编写的Calculator类和它的单元测试案例类CalculatorTest放在对应的目录中。
第二步:编写我们的build.gradle文件,如下
第三步:执行gradle命令。命令行模式下,进入我们刚才仙剑的“gradle-junit”工程所在的目录,执行gradle test命令。等待gradle构建完毕,从窗口可以看到本次执行的结果,如下:
结果分析解读:
1. 执行testSquareRoot(死循环)测试失败,并报TestTimedOutException异常;
2. 总共测试了7个tests,1个失败了(failed),1个跳过了(skipped);
3. 最后最大的亮点在,gradle为我们生成了本次测试的html页面报告文件(…/build/reports/tests/test/index.html),可以非常直观的显示本次构建结果,并且还支持链接查看类和我们刚才编写的单元测试案例。
4. 点击红色标记的包或者类可以进入相应的链接页面,查看更详细的结果。例如点击下面的CalculatorTest链接可以查看测试失败单元测试案例原因:
TestTimedOutException:test timed out after 1000 milliseconds
以及测试类的一个测试结果类的小结,如下图:
(可以看见执行testSquareRoot测试任务花费1.010s,超过我们设定的阀值1s;
并且testMultiply测试任务变色。)
4 单元测试代码覆盖率
在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,有用代码覆盖率来考核测试任务完成情况,比如代码覆盖率必须达到80%或90%。正确理解代码覆盖率,对我们设计案例覆盖代码很有必要,因为仅仅使用代码覆盖率来衡量,有利也有弊。
4.1 语句覆盖
语句覆盖就是度量被测试代码中每个可执行语句是否被执行到了,简单的说就是只统计能够执行的代码被执行了多少行。语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中执行语句,却不考虑各种分支的组合等等。例如:
测试代码如下:
int foo (int a,int b)
{
return a/b;
}
假如我们设计如下的测试案例:
TeseCase:a = 10,b = 5
虽然代码覆盖率到达了100%,并且所有的测试案例都通过了。然而遗憾的是,我们的语句覆盖率到达了所谓的100%,但是却没有发现最贱的bug,比如当我们取b=0时,会抛出异常。再如下面的例子:
int foo (int a, int b)
{
int result = 0
if (a < 10) {
result += 1;
}
if (b < 10) {
result += 10;
}
return resule;
}
设计的测试案例如下:
TeseCase:a = 5, b = 5
该案例保证了测试案例的语句都执行了,包括分支中执行的语句,因此语句覆盖率也达到了100%,但是从分支逻辑上明显覆盖不全。
4.2 判定覆盖和条件覆盖
判定覆盖又称分支覆盖,它是度量程序中没一个判定的分支是否都被测试到了。这句话需要进一步理解,很容易和条件覆盖混淆。我们看例子:
int foo (int a, int b)
{
if (a < 10 || b < 10)
{
return 0; //分支一
}else {
return 1;//分支二
}
}
设计判定覆盖案例时,我们只需考虑判定结果为true和false两种情况,我们设计如下的案例就能达到判定覆盖率为100%:
TeseCase1:a = 5, b = 任意数 覆盖了分支一
TeseCase2:a = 15, b = 15 覆盖了分支二
从设计条件的逻辑上出发,TeseCase2的设计与TeseCase1的在逻辑上完全的互斥互逆的,简单理解为除了分支一的情况,就只剩下分支二了。
设计条件覆盖案例时,我们需要考虑判定中的每个条件表达结果,为了覆盖率达到100%,我们设计如下的案例:
TeseCase1:a = 5, b = 5 true, true
TeseCase2:a = 15,b = 15 false, false
由此可见,条件覆盖不是讲判定中的每一个田间表达式的结果进行排列组合,而只要每个条件表达式的结果为true和false测试到了就OK了。说白了,条件覆盖就是不考虑逻辑分支仅覆盖条件分支,执行条件分支语句的覆盖。因此,我们得出这样的推论:完全的条件覆盖并不能覆盖保证完全的判定覆盖。
4.3 路径覆盖
对于路径覆盖的理解,我们设计如下的案例:
int foo (int a, int b)
{
int result = 0
if (a < 10) {
result += 1;
}
if (b < 10) {
result += 10;
}
return resule;
}
测试案例:
TsseCase1:a = 5, b = 5
TeseCase2:a = 5, b = 15
TeseCase3:a = 15, b = 5
TeseCase4:a = 15, b = 15
路径覆盖又称断言覆盖。它度量了是否函数的每一个分支都被执行了。这句话也非常好理解,就是所有的可能分支都执行一遍,有多个分支嵌套时,需要对多个分支进行排列组合,可想而知,测试的路径随着分支的数量呈2ⁿ指数级别的增加。
5 Jacoco单元测试覆盖率
Jacoco专门用于统计单元测试覆盖率的,在gradle中和‘java’插件一样使用时直接引用,并添加版本即可:
apply plugin: 'jacoco'
jacoco{ toolVersion = "0.7.1.201405082137" }
运行jacoco也非常简单,因为他依赖于单元测试的完成,所以执行完test任务后,可以紧接着执行gradle jacoco命令,执行成功,也会生成一个类似单元测试的html结果报告,非常直观的可以看到统计数据的结果。
5.1 Jacoco案例分析(一):语句覆盖与判定覆盖
源码如下:
package com.huawei.demo;
public class CoverJudge
{
public int foot(int a, int b)
{
int result = 0;
if (a < 10)
{
result += 1;
}
if (b < 10)
{
result += 10;
}
return result;
}
}
下面是设计的单元测试案例:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Test;
import com.huawei.demo.CoverJudge;
public class CoverJudgeTest
{
private static CoverJudge coverJudge = new CoverJudge();
//简单的语句覆盖
@Test
public void testFoot()
{
int a = 5;
int b = 5;
assertEquals(11, coverJudge.foot(a, b));
}
/*@Test
public void testFoot()
{
int a = 5;
int b = 15;
assertEquals(1, coverJudge.foot(a, b));
}
@Test
public void testFoot1()
{
int a = 15;
int b = 5;
assertEquals(10, coverJudge.foot(a, b));
}*/
}
运行gradle test jacoco,构建完成后可以查看覆盖率报告,如下:
显示分支覆盖率(Missed Branches)仅为50%。
5.2 Jacoco案例分析(二):多分支覆盖
源码如下:
package com.huawei.demo;
public class MathUtil
{
public int max(int a, int b, int c)
{
if (a > b)
{
if (a > c)
{
return a;
}
else
{
return c;
}
}
else
{
if (b > c)
{
return b;
}
else
{
return c;
}
}
}
}
下面是设计的单元测试案例:
TeseCase1:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Test;
import com.huawei.demo.MathUtil;
public class MathUtilTest
{
private static MathUtil mathUtil = new MathUtil();
@Test
public void testMax1_2_3()
{
assertEquals(3, mathUtil.max(1, 2, 3));
}
}
执行后结果如下,还可以进入链接查看详情
TestCase2:
package com.huawei.demoTest;
import static org.junit.Assert.*;
import org.junit.Test;
import com.huawei.demo.MathUtil;
public class MathUtilTest2
{
private static MathUtil mathUtil = new MathUtil();
@Test
public void testMax1_2_3()
{
assertEquals(3, mathUtil.max(1, 2, 3));
}
@Test
public void test_max_1_3_2() {
assertEquals(3, mathUtil.max(1, 3, 2));
}
@Test
public void test_max_3_2_1() {
assertEquals(3, mathUtil.max(3, 2, 1));
}
@Test
public void test_max_0_0_0(){
assertEquals(0, mathUtil.max(0, 0, 0));
}
@Test
public void test_max_0_1_0(){
assertEquals(1, mathUtil.max(0, 1, 0));
}
}
执行后结果如下:
点击链接还可以查询没有覆盖到的分支:
红色表示没有覆盖到的分支;分析没有覆盖到a > b且a < c的情况,所以可以设计如下Tesecase:a = 2, b = 1, c = 3的情况。
所以增加如下单元测试案例:
@Test
public void test_max_2_1_3() {
assertEquals(3, mathUtil.max(2, 1, 3));
}
再次执行可以显示100%的分支代码覆盖率。