单元测试是编写测试代码,用来检测特定的、明确的、细颗粒的功能。单元测试并不必定保证程序功能是正确的,更不保证总体业务是准备的。html
单元测试不只仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构以后的正确性。java
通常来讲,单元测试任务包括web
接口功能测试:用来保证接口功能的正确性。session
局部数据结构测试(不经常使用):用来保证接口中的数据结构是正确的数据结构
好比变量有无初始值框架
变量是否溢出socket
边界条件测试ide
好比对于Math.sqrt,给出n^2-1,和n^2+1的边界工具
空集合post
对集合的大小应用“数值变量”的边界
调整次序:升序、降序
引用“字符变量”的边界
空字符串
对字符串长度应用“数值变量”的边界
主要边界:最小值,最大值,无穷大(对于DOUBLE等)
溢出边界(指望异常或拒绝服务):最小值-1,最大值+1
临近边界:最小值+1,最大值-1
变量没有赋值(即为NULL)
变量是数值(或字符)
变量是字符串
变量是集合
变量有规律
全部独立执行通路测试:保证每一条代码,每一个分支都通过测试
Cobertura:语句覆盖
Emma: Eclipse插件Eclemma
语句覆盖:保证每个语句都执行到了
断定覆盖(分支覆盖):保证每个分支都执行到
条件覆盖:保证每个条件都覆盖到true和false(即if、while中的条件语句)
路径覆盖:保证每个路径都覆盖到
代码覆盖率
相关软件
各条错误处理通路测试:保证每个异常都通过测试
JUnit是Java单元测试框架,已经在Eclipse中默认安装。目前主流的有JUnit3和JUnit4。JUnit3中,测试用例须要继承TestCase
类。JUnit4中,测试用例无需继承TestCase
类,只须要使用@Test
等注解。
先看一个Junit3的样例
// 测试java.lang.Math
// 必须继承TestCase
public class Junit3TestCase extends TestCase {
public Junit3TestCase() {
super();
}
// 传入测试用例名称
public Junit3TestCase(String name) {
super(name);
}
// 在每一个Test运行以前运行
@Override
protected void setUp() throws Exception {
System.out.println("Set up");
}
// 测试方法。
// 方法名称必须以test开头,没有参数,无返回值,是公开的,能够抛出异常
// 也即相似public void testXXX() throws Exception {}
public void testMathPow() {
System.out.println("Test Math.pow");
Assert.assertEquals(4.0, Math.pow(2.0, 2.0));
}
public void testMathMin() {
System.out.println("Test Math.min");
Assert.assertEquals(2.0, Math.min(2.0, 4.0));
}
// 在每一个Test运行以后运行
@Override
protected void tearDown() throws Exception {
System.out.println("Tear down");
}
}
若是采用默认的TestSuite,则测试方法必须是public void testXXX() [throws Exception] {}
的形式,而且不能存在依赖关系,由于测试方法的调用顺序是不可预知的。
上例执行后,控制台会输出
Set up
Test Math.pow
Tear down
Set up
Test Math.min
Tear down
从中,能够猜想到,对于每一个测试方法,调用的形式是:
testCase.setUp();
testCase.testXXX();
testCase.tearDown();
在Eclipse中,能够直接在类名或测试方法上右击,在弹出的右击菜单中选择Run As -> JUnit Test。
在Mvn中,能够直接经过mvn test
命令运行测试用例。
也能够经过Java方式调用,建立一个TestCase
实例,而后重载runTest()
方法,在其方法内调用测试方法(能够多个)。
TestCase test = new Junit3TestCase("mathPow") {
// 重载
protected void runTest() throws Throwable {
testMathPow();
};
};
test.run();
更加便捷地,能够在建立TestCase
实例时直接传入测试方法名称,JUnit会自动调用此测试方法,如
TestCase test = new Junit3TestCase("testMathPow");
test.run();
TestSuite是测试用例套件,可以运行过个测试方法。若是不指定TestSuite,会建立一个默认的TestSuite。默认TestSuite会扫描当前内中的全部测试方法,而后运行。
若是不想采用默认的TestSuite,则能够自定义TestSuite。在TestCase中,能够经过静态方法suite()
返回自定义的suite。
import junit.framework.Assert;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class Junit3TestCase extends TestCase {
//...
public static Test suite() {
System.out.println("create suite");
TestSuite suite = new TestSuite();
suite.addTest(new Junit3TestCase("testMathPow"));
return suite;
}
}
容许上述方法,控制台输出
写道
create suite
Set up
Test Math.pow
Tear down
而且只运行了testMathPow
测试方法,而没有运行testMathMin
测试方法。经过显式指定测试方法,能够控制测试执行的顺序。
也能够经过Java的方式建立TestSuite,而后调用TestCase,如
// 先建立TestSuite,再添加测试方法
TestSuite testSuite = new TestSuite();
testSuite.addTest(new Junit3TestCase("testMathPow"));
// 或者 传入Class,TestSuite会扫描其中的测试方法。
TestSuite testSuite = new TestSuite(Junit3TestCase.class,Junit3TestCase2.class,Junit3TestCase3.class);
// 运行testSuite
TestResult testResult = new TestResult();
testSuite.run(testResult);
testResult中保存了不少测试数据,包括运行测试方法数目(runCount
)等。
与JUnit3不一样,JUnit4经过注解的方式来识别测试方法。目前支持的主要注解有:
@BeforeClass
全局只会执行一次,并且是第一个运行
@Before
在测试方法运行以前运行
@Test
测试方法
@After
在测试方法运行以后容许
@AfterClass
全局只会执行一次,并且是最后一个运行
@Ignore
忽略此方法
下面举一个样例:
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
public class Junit4TestCase {
@BeforeClass
public static void setUpBeforeClass() {
System.out.println("Set up before class");
}
@Before
public void setUp() throws Exception {
System.out.println("Set up");
}
@Test
public void testMathPow() {
System.out.println("Test Math.pow");
Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);
}
@Test
public void testMathMin() {
System.out.println("Test Math.min");
Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);
}
// 指望此方法抛出NullPointerException异常
@Test(expected = NullPointerException.class)
public void testException() {
System.out.println("Test exception");
Object obj = null;
obj.toString();
}
// 忽略此测试方法
@Ignore
@Test
public void testMathMax() {
Assert.fail("没有实现");
}
// 使用“假设”来忽略测试方法
@Test
public void testAssume(){
System.out.println("Test assume");
// 当假设失败时,则会中止运行,但这并不会意味测试方法失败。
Assume.assumeTrue(false);
Assert.fail("没有实现");
}
@After
public void tearDown() throws Exception {
System.out.println("Tear down");
}
@AfterClass
public static void tearDownAfterClass() {
System.out.println("Tear down After class");
}
}
若是细心的话,会发现Junit3的package是junit.framework
,而Junit4是org.junit
。
执行此用例后,控制台会输出
写道
Set up before class
Set up
Test Math.pow
Tear down
Set up
Test Math.min
Tear down
Set up
Test exception
Tear down
Set up
Test assume
Tear down
Tear down After class
能够看到,执行次序是@BeforeClass
-> @Before
-> @Test
-> @After
-> @Before
-> @Test
-> @After
-> @AfterClass
。@Ignore
会被忽略。
与Junit3相似,能够在Eclipse中运行,也能够经过mvn test
命令运行。
Junit3和Junit4都提供了一个Assert类(虽然package不一样,可是大体差很少)。Assert类中定义了不少静态方法来进行断言。列表以下:
assertTrue(String message, boolean condition) 要求condition == true
assertFalse(String message, boolean condition) 要求condition == false
fail(String message) 必然失败,一样要求代码不可达
assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)
assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
assertNotNull(String message, Object object) 要求object!=null
assertNull(String message, Object object) 要求object==null
assertSame(String message, Object expected, Object actual) 要求expected == actual
assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true
Mock和Stub是两种测试代码功能的方法。Mock测重于对功能的模拟。Stub测重于对功能的测试重现。好比对于List接口,Mock会直接对List进行模拟,而Stub会新建一个实现了List的TestList,在其中编写测试的代码。
强烈建议优先选择Mock方式,由于Mock方式下,模拟代码与测试代码放在一块儿,易读性好,并且扩展性、灵活性都比Stub好。
比较流行的Mock有:
其中EasyMock和Mockito对于Java接口使用接口代理的方式来模拟,对于Java类使用继承的方式来模拟(也即会建立一个新的Class类)。Mockito支持spy方式,能够对实例进行模拟。但它们都不能对静态方法和final类进行模拟,powermock经过修改字节码来支持了此功能。
IBM上有几篇介绍EasyMock使用方法和原理的文章:EasyMock 使用方法与原理剖析,使用 EasyMock 更轻松地进行测试。
EasyMock把测试过程分为三步:录制、运行测试代码、验证指望。
录制过程大概就是:指望method(params)执行times次(默认一次),返回result(可选),抛出exception异常(可选)。
验证指望过程将会检查方法的调用次数。
一个简单的样例是:
@Test
public void testListInEasyMock() {
List list = EasyMock.createMock(List.class);
// 录制过程
// 指望方法list.set(0,1)执行2次,返回null,不抛出异常
expect1: EasyMock.expect(list.set(0, 1)).andReturn(null).times(2);
// 指望方法list.set(0,1)执行1次,返回null,不抛出异常
expect2: EasyMock.expect(list.set(0, 1)).andReturn(1);
// 执行测试代码
EasyMock.replay(list);
// 执行list.set(0,1),匹配expect1指望,会返回null
Assert.assertNull(list.set(0, 1));
// 执行list.set(0,1),匹配expect1(由于expect1指望执行此方法2次),会返回null
Assert.assertNull(list.set(0, 1));
// 执行list.set(0,1),匹配expect2,会返回1
Assert.assertEquals(1, list.set(0, 1));
// 验证指望
EasyMock.verify(list);
}
EasyMock还支持严格的检查,要求执行的方法次序与指望的彻底一致。
Mockito是Google Code上的一个开源项目,Api相对于EasyMock更好友好。与EasyMock不一样的是,Mockito没有录制过程,只须要在“运行测试代码”以前对接口进行Stub,也即设置方法的返回值或抛出的异常,而后直接运行测试代码,运行期间调用Mock的方法,会返回预先设置的返回值或抛出异常,最后再对测试代码进行验证。能够查看此文章了解二者的不一样。
官方提供了不少样例,基本上包括了全部功能,能够去看看。
这里从官方样例中摘录几个典型的:
验证调用行为
import static org.mockito.Mockito.*;
//建立Mock
List mockedList = mock(List.class);
//使用Mock对象
mockedList.add("one");
mockedList.clear();
//验证行为
verify(mockedList).add("one");
verify(mockedList).clear();
对Mock对象进行Stub
//也能够Mock具体的类,而不只仅是接口
LinkedList mockedList = mock(LinkedList.class);
//Stub
when(mockedList.get(0)).thenReturn("first"); // 设置返回值
when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出异常
//第一个会打印 "first"
System.out.println(mockedList.get(0));
//接下来会抛出runtime异常
System.out.println(mockedList.get(1));
//接下来会打印"null",这是由于没有stub get(999)
System.out.println(mockedList.get(999));
// 能够选择性地验证行为,好比只关心是否调用过get(0),而不关心是否调用过get(1)
verify(mockedList).get(0);
比较流行的工具是Emma和Jacoco,Ecliplse插件有eclemma。eclemma2.0以前采用的是Emma,以后采用的是Jacoco。这里主要介绍一下Jacoco。Eclmama因为是Eclipse插件,因此很是易用,就很少作介绍了。
Jacoco能够嵌入到Ant、Maven中,也可使用Java Agent技术监控任意Java程序,也可使用Java Api来定制功能。
Jacoco会监控JVM中的调用,生成监控结果(默认保存在jacoco.exec文件中),而后分析此结果,配合源代码生成覆盖率报告。须要注意的是:监控和分析这两步,必须使用相同的Class文件,不然因为Class不一样,而没法定位到具体的方法,致使覆盖率均为0%。
首先,须要下载jacocoagent.jar文件,而后在Java程序启动参数后面加上 -javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]
,具体的options能够在此页面找到。默认会在JVM关闭时(注意不能是kill -9
),输出监控结果到jacoco.exec文件中,也能够经过socket来实时地输出监控报告(能够在Example代码中找到简单实现)。
可使用Ant、Mvn或Eclipse来分析jacoco.exec文件,也能够经过API来分析。
public void createReport() throws Exception {
// 读取监控结果
final FileInputStream fis = new FileInputStream(new File("jacoco.exec"));
final ExecutionDataReader executionDataReader = new ExecutionDataReader(fis);
// 执行数据信息
ExecutionDataStore executionDataStore = new ExecutionDataStore();
// 会话信息
SessionInfoStore sessionInfoStore = new SessionInfoStore();
executionDataReader.setExecutionDataVisitor(executionDataStore);
executionDataReader.setSessionInfoVisitor(sessionInfoStore);
while (executionDataReader.read()) {
}
fis.close();
// 分析结构
final CoverageBuilder coverageBuilder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(executionDataStore, coverageBuilder);
// 传入监控时的Class文件目录,注意必须与监控时的同样
File classesDirectory = new File("classes");
analyzer.analyzeAll(classesDirectory);
IBundleCoverage bundleCoverage = coverageBuilder.getBundle("Title");
// 输出报告
File reportDirectory = new File("report"); // 报告所在的目录
final HTMLFormatter htmlFormatter = new HTMLFormatter(); // HTML格式
final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory));
// 必须先调用visitInfo
visitor.visitInfo(sessionInfoStore.getInfos(), executionDataStore.getContents());
File sourceDirectory = new File("src"); // 源代码目录
// 遍历全部的源代码
// 若是不执行此过程,则在报告中只能看到方法名,可是没法查看具体的覆盖(由于没有源代码页面)
visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4));
// 执行完毕
visitor.visitEnd();
}