转自阿里巴巴墨源:Java服务端单元测试指南
一. 前言
单元测试并不只是为了验证你当前所写的代码是否存在问题,更为重要的是它可以很大程度的保障日后因业务变更、修复Bug
或重构等引起的代码变更而导致(或新增)的风险。
同时将单元测试提前到编写正式代码进行(测试驱动开发),可以很好的提高对代码结构的设计。通过优先编写测试用例,可以很好的从用户角度来对功能的分解、使用过程和接口等进行设计,从而提高代码结构的高内聚、低耦合特性。使得对日后的需求变更或代码重构等更加高效、简洁。
因此编写单元测试对产品开发和维护、技术提升和积累具有重大意义!
二. 第一个单元测试
首先写一个单元测试,这样有助于对后面内容的理解与实践。
2.1 开发环境
IntelliJ IDEA IntelliJ IDEA
默认自带并启用TestNG
和覆盖率插件:
-
- TestNG
在设置窗口查看TestNG插件是否安装与启用:
-
- 覆盖率
同样,查看覆盖率插件可以搜索“Coverage”。IntelliJ IDEA的覆盖率统计工具有三种,JaCoCo、Emma和IntelliJ IDEA自带。
-
- 变异测试
同样,查看并安装变异测试插件可以搜索“PIT mutation testing”。
Eclipse Eclipse
需要自行安装单元测试相关插件:
-
- TestNG
执行TestNG单元测试的插件。可在Eclipse Marketplace搜索“TestNG”安装:
-
- 覆盖率
获取单元测试覆盖率的插件。可在Eclipse Marketplace搜索“EclEmma”安装:
-
- 变异测试
同样,查看并安装变异测试插件可以搜索“Pitclipse”。
2.2 Maven依赖
在这里搜索JAR
包的新版本
- TestNG
- JMockit
- Spring Test
- 其他(或许需要)
2.3 创建单元测试
下面介绍通过IDE
自动创建单元测试的方法(也可手动完成):
IntelliJ IDEA
-
- 1.在被测试类的类名代码行上(“class”关键字代码行)按下Alt + Enter快捷键(或将鼠标指针停留其上,待出现黄色灯泡图标后,鼠标点击其下拉菜单。),在弹出的菜单上选择Create Test选项:
-
- 2.在弹出的窗口中选择“TestNG”并选择要创建的单元测试方法后点击“OK”按钮创建单元测试。(中间的类名、包名以及是否要创建“setUP”和“tearDown”方法,可根据自己的情况选择。)
-
- 3.创建后的单元测试在Maven工程的test目录下生成测试类:
Eclipse:
-
- 1.在被测试类的文件上右键菜单“New -> Other”:
2.在弹出的窗口中搜索“Test”,选择“TestNG class”后点击“Next”按钮:
3.在窗口中选择要创建的测试方法后点击“Next”按钮:
4.根据自己的情况设置包名、类名和Annotations等:
示例代码
可参考下例代码编写单元测试:
2.4 运行单元测试
IntelliJ IDEA
-
- 1.在测试方法上鼠标右键“Run ‘testMethod()’”,在同级菜单下面还有“Debug”和“Coverage”:
-
- 注:也可点击工具栏选项运行,从左至右依次是:运行、调试、覆盖率运行,可根据自己的需要选择:
-
- 2.点击“运行”:
-
- 左侧框:单元测试类区域
- 底侧框:单元测试打印输出的内容和运行结果
Eclipse
-
- 1.在测试方法上鼠标右键“Run As -> TestNG Test”,在同级菜单下面还有“Debug As”和“Coverage As”:
注:也可点击工具栏选项运行,从左至右依次是:覆盖率、调试、运行运行。
2.点击“运行”:
左侧框:单元测试运行结果
底侧框:单元测试打印输出的内容
Maven
- 执行目录下所有单元测试,进入工程目录后执行:mvn test
- 执行具体的单元测试类,多个测试类可用逗号分开:mvn test -Dtest=Test1,Test2
- 执行具体的单元测试类的方法:mvn test -Dtest=Test1#testMethod
- 执行某个包下的单元测试:mvn test -Dtest=com/alibaba/biz/*
- 执行ANT风格路径表达式下的单元测试:mvn test -Dtest=**/*Test或mvn test -Dtest=**/???Test
- 忽略单元测试:mvn -Dmaven.test.skip=true
2.5 单元测试覆盖
IntelliJ IDEA
-
- 1.运行
-
- 点击“覆盖率运行”:
-
- 左侧框1:被测试类和覆盖率
-
- 左侧框2:单元测试类区域
-
- 中间框:被测试类代码。绿色行表示被覆盖,红色行表示末被覆盖
-
- 右侧框:所有类被覆盖情况,双击包名可以查看详细类覆盖情况
-
- 底侧框:单元测试打印输出的内容和运行结果
-
- 2.输出报告
- 运行过程以及结果输出的窗口中有一行“JMockit: Coverage report written to”,是JMocit创建的覆盖率报告文件目录:
Eclipse
-
- 1.运行
-
- 点击“覆盖率运行”(可参看“运行单元测试”一节):
-
- 左侧框:运行结果
-
- 中间框:被测试类代码。绿色行表示被覆盖,红色行表示末被覆盖
-
- 右侧框:所有类被覆盖情况,双击包名可以查看详细类覆盖情况(相比IntelliJ IDEA的Package窗口缺少了类覆盖率数据)
-
- 底侧框:单元测试打印输出的内容
2.输出报告
运行过程以及结果输出的窗口中有一行“JMockit: Coverage report written to”,是EclEmma创建的覆盖率报告文件目录:
覆盖率报告
-
- 打开目录下的“index.html”文件:
- 点击查看类文件详细覆盖情况:
2.6 变异测试
变异测试是覆盖率的一个很好的补充。相比覆盖率,它能够使单元测试更加健壮。(具体可见5.4节)
IntelliJ IDEA
-
- 1. 创建运行
-
- 点击运行的下拉选项,选择“Edit Configurations…”打开“Run/Debug Configurations”窗口:
-
- 点击左上角“+”号后选择添加“PIT Runner”:
-
- 注意“Target classes 和 Source dir”两项与单元测试类包和模块路径的准确对应,确认无误后点击“OK”按钮确认创建。
-
- 2. 运行
-
- 创建完成后,直接点击工具条“运行”图标即可运行。
3. 输出报告
运行过程以及结果输出的窗口中最后一行“Open report in browser”即为插件创建的报告连接。
点击即可打开报告:
Eclipse
-
- 1. 运行
-
- 在单元测试类上右键菜单“Run As” -> “2 PIT Mutation Test”,即可运行:
-
- 运行过程以及结果输出的窗口:
2. 输出报告
可在此窗口中查看变异测试发现的可能存在的代码缺陷:(这点比IDEA的PIT插件做的要好)
可在此窗口中查看测试报告:
为今后更好的开展与落实单元测试,请继续阅读下面内容。
3 单元测试框架
3.1 TestNG
Junit4
和TestNG
是Java
非常流行的单元测试框架。因TestNG
更加简洁、灵活和功能丰富,所以我们选用TestNG
。
下面通过与Junit4
的比较来了解一下TestNG
的特性:
注解支持
Junit4
和TestNG
的注解对比:
特性 | JUnit4 | TestNG |
---|---|---|
测试注解 | @Test | @Test |
在测试套件执行之前执行 | – | @BeforeSuite |
在测试套件执行之后执行 | – | @AfterSuite |
在测试之前执行 | – | @BeforeTest |
在测试之后执行 | – | @AfterTest |
在测试组执行之前执行 | – | @BeforeGroups |
在测试组执行之后执行 | – | @AfterGroups |
在测试类执行之前执行 | @BeforeClass | @BeforeClass |
在测试类执行之后执行 | @AfterClass | @AfterClass |
在测试方法执行之前执行 | @Before | @BeforeMethod |
在测试方法执行之后执行 | @After | @AfterMethod |
忽略测试 | @ignore | @Test(enbale=false) |
预期异常 | @Test(expected = Exception.class) | @Test(expectedExceptions = Exception.class) |
超时 | @Test(timeout = 1000) | @Test(timeout = 1000) |
// TODO 测试 测试方法 测试套件 测试组 的区别
在Junit4
中,@BeforeClass
和@AfterClass
只能用于静态方法。TestNG
无此约束。
异常测试
异常测试是指在单元测试中应该要抛出什么异常是合理的。
- JUnit4
- TestNG
忽略测试
忽略测试是指这个单元测试可以被忽略。
- JUnit4
- TestNG
时间测试
时间测试是指一个单元测试运行的时间超过了指定时间(毫秒数),那么测试将失败。
- JUnit4
- TestNG
套件测试
套件测试是指把多个单元测试组合成一个模块,然后统一运行。
- JUnit4
@RunWith
和@Suite
注解被用于执行套件测试。下面的代码是所展示的是在“JunitTest5
”被执行之后需要“JunitTest1
”和“JunitTest2
”也一起执行。所有的声明需要在类内部完成。
java
- TestNG
是使用XML
配置文件来执行套件测试。下面的配置将“TestNGTest1
”和“TestNGTest2
”一起执行。
TestNG
的另一种方式使用了组的概念,每个测试方法都可以根据功能特性分配到一个组里面。例如:
这是一个有4个方法,3个组(method1, method2 和 method4)的类。使用起来比XML的套件更简洁。
下面XML
文件配置了一个执行组为methed1
的单元测试。
分组使集成测试更加强大。例如,我们可以只是执行所有测试中的组名为DatabaseFuntion
的测试。
参数化测试
参数化测试是指给单元测试传多种参数值,验证接口对多种不同参数的处理是否正确。
- JUnit4
@RunWith
和@Parameter
注解用于为单元测试提供参数值,@Parameters
必须返回List
,参数将会被作为参数传给类的构造函数。
它的使用很不方便:一个方法的参数化测试必须定义一个测试类。测试参数通过一个注解为@Parameters且返回值为List参数值列表的静态方法。然后将方法返回值成员通过类的构造函数初始化为类的成员。最后再将类的成员做为参数去测试被测试方法。
- TestNG
使用XML
文件或@DataProvider注解两种方式为测试提供参数。
XML
文件配置参数化测试
方法上添加@Parameters注解,参数数据由TestNG的XML配置文件提供。这样做之后,我们可以使用不同的数据集甚至是不同的结果集来重用一个测试用例。另外,甚至是最终用户,QA或者QE可以提供他们自己的XML文件来做测试。
XML 文件
@DataProvider注解参数化测试
使用XML
文件初始化数据虽然方便,但仅支持基础数据类型。如需复杂的类型可使用@DataProvider
注解解决。
@DataProvider作为对象的参数
P.S “TestNGTest6_3_0” 是一个简单的对象,使用了get和set方法。
TestNG的参数化测试使用起来非常方便,它可以在一个测试类中添加多个方法的参数化测试(JUnit4一个方法就需要一个类)。
依赖测试
依赖测试是指测试的方法是有依赖的,在执行的测试之前需要执行的另一测试。如果依赖的测试出现错误,所有的子测试都被忽略,且不会被标记为失败。
- JUnit4
JUnit4框架主要聚焦于测试的隔离,暂时还不支持这个特性。
- TestNG
它使用dependOnMethods来实现了依赖测试的功能,如下:
如果method1()成功执行,那么method2()也将被执行,否则method2()将会被忽略。
性能测试
TestNG
支持通过多个线程并发调用一个测试接口来实现性能测试。JUnit4
不支持,若要进行性能测试需手动添加并发代码。
并行测试
TestNG
支持通过多个线程并发调用多个测试接口执行测试,相对于传统的单线程执行测试的方式,可以很大程度减少测试运行时间。
并行测试配置:
讨论总结
通过上面的对比,建议使用TestNG作为Java项目的单元测试框架,因为TestNG在参数化测试、依赖测试以、套件测试(组)及并发测试方面功能更加简洁、强大。另外,TestNG也涵盖了JUnit4的全部功能。
3.2 JMockit
Mock的使用场景:
比如Mock以下场景:
-
- 1. 外部依赖的应用的调用,比如WebService等服务依赖。
-
- 2. DAO层(访问MySQL、Oracle、Emcache等底层存储)的调用等。
-
- 3. 系统间异步交互通知消息。
-
- 4. methodA里面调用到的methodB。
- 5. 一些应用里面自己的Class(abstract,final,static)、Interface、Annotation、Enum和Native等。
Mock工具的原理:
Mock
工具工作的原理大都如下:
-
- 1. Record阶段:录制期望。也可以理解为数据准备阶段。创建依赖的Class或Interface或Method,模拟返回的数据、耗时及调用的次数等。
-
- 2. Replay阶段:通过调用被测代码,执行测试。期间会Invoke到第一阶段Record的Mock对象或方法。
- 3. Verify阶段:验证。可以验证调用返回是否正确,及Mock的方法调用次数,顺序等。
当前的一些Mock工具的比较:
历史曾经或当前比较流行的Mock工具有EasyMock
、jMock
、Mockito
、Unitils Mock
、PowerMock
、JMockit
等工具。
他们的功能对比如下:
http://billben.iteye.com/blog/1872196
从这里可以看到,JMockit
的的功能最全面、强大!所以我们单元测试中的Mock工具也选择了JMockit。同时在开发的过程中,JMockit的“Auto-injection of mocks”及“Special fields for “any” argument matching”及各种有用的Annotation使单元测试的开发更简洁和高效。
JMockit的简介:
JMockit
是用以帮助开发人员编写单元测试的Mock
工具。它基于java.lang.instrument包开发,并使用ASM库来修改Java的Bytecode。正因此两点,它可以实现无所不能的Mock。
JMockit可以Mock的种类包含了:
- class(abstract, final, static)
- interface
- enum
- annotation
- native
JMockit有两种Mock的方式:
- Behavior-oriented(Expectations & Verifications)
- State-oriented(MockUp)
通俗点讲,Behavior-oriented是基于行为的Mock,对Mock目标代码的行为进行模仿,像是黑盒测试。State-oriented是基于状态的Mock,是站在目标测试代码内部的。可以对传入的参数进行检查、匹配,才返回某些结果,类似白盒。而State-oriented的new MockUp基本上可以Mock任何代码或逻辑。
以下是JMockit的APIs和tools:
可以看到JMockit常用的Expectation、StrictExpectations和NonStrictExpectations期望录制及注解@Tested、@Mocked,@NonStrict、@Injectable等简洁的Mock代码风格。而且JMockit还自带了Code Coverage的工具供本地单元测试时候逻辑覆盖或代码覆盖率使用。
JMockit的使用:
以“第一个单元测试”代码为例:
- 测试对象
@Tested:JMockit会自动创建注解为“@Tested”的类对象,并将其做为被测试对象。 通过设置“availableDuringSetup=true”参数,可以使得被测试对象在“setUp”方法执行前被创建出来。
- Mock对象
@Injectable:JMockit自动创建注解为“@Injectable”的类对象,并将其自动注入被测试对象。
相关的注解还有:// TODO 待补充
-
- @Mocked:
-
- @Capturing:
- @Cascading:
- 录制
Expectations:块里的内容是用来Mock方法,并指定方法的返回值、异常、调用次数和耗时。此块中的方法是必须被执行的,否则单元测试失败。
相关的类还有:
-
- StrictExpectations:块里声明的Mock方法,是必须按照先后顺序被执行的。
-
- NonStrictExpectations:块里声明的Mock方法,是可以不被执行的。
- MockUP:可以添加到Expectations块中,用来Mock静态类、接口等具体类型的方法实现。也可以独立于Expectations块单独使用,不可放置Expectations块外部使用(即不可与Expectations同层、同时使用)。
- 结果验证
Assert:是最常见的断言验证
Verifications:一种特殊的验证块。比如:要验证一个被测试类中,调用的某个方法是否为指定的参数、调用次数。相比Expectations它放在单元测试的最后且没有Mock功能。
注:以上列举的注释具体用法示例请查阅第7节内容
4 单元测试内容
在单元测试时,测试人员根据设计文档和源码,了解模块的接口和逻辑结构。主要采用白盒测试用例,辅之黑盒测试用例,使之对任何(合理和不合理)的输入都要能鉴别和响应。这就要求对程序所有的局部和全局的数据结构、外部接口和程序代码的关键部分进行检查。
在单元测试中主要在5个方面对被测模块进行检查。
4.1 模块接口测试
在单元测试开始时,应该对所有被测模块的接口进行测试。如果数据不能正常地输入和输出,那么其他的测试毫无意义。Myers在关于软件测试的书中为接口测试提出了一个检查表:
- 模块输入参数的数目是否与模块形式参数数目相同
- 模块各输入的参数属性与对应的形参属性是否一致
- 模块各输入的参数类型与对应的形参类型是否一致
- 传到被调用模块的实参的数目是否与被调用模块形参的数目相同
- 传到被调用模块的实参的属性是否与被调用模块形参的属性相同
- 传到被调用模块的实参的类型是否与被调用模块形参的类型相同
- 引用内部函数时,实参的次序和数目是否正确
- 是否引用了与当前入口无关的参数
- 用于输入的变量有没有改变
- 在经过不同模块时,全局变量的定义是否一致
- 限制条件是否以形参的形式传递
- 使用外部资源时,是否检查可用性并及时释放资源,如内存、文件、硬盘、端口等
当模块通过外部设备进行输入/输出操作时,必须扩展接口测试,附加如下的测试项目:
- 文件的属性是否正确
- Open与Close语句是否正确
- 规定的格式是否与I/O语句相符
- 缓冲区的大小与记录的大小是否相配合
- 在使用文件前,文件是否打开
- 文件结束的条件是否会被执行
- I/O错误是否检查并做了处理
- 在输出信息中是否有文字错误
4.2 局部数据结构测试
模块的局部数据结构是最常见的错误来源,应设计测试用例以检查以下各种错误:
- 不正确或不一致的数据类型说明
- 使用尚未赋值或尚未初始化的变量
- 错误的初始值或错误的默认值
- 变量名拼写错或书写错——使用了外部变量或函数
- 不一致的数据类型
- 全局数据对模块的影响
- 数组越界
- 非法指针
4.3 路径测试
检查由于计算、判定和控制流错误而导致的程序错误。由于在测试时不可能做到穷举测试,所以在单元测试时要根据“白盒”测试和“黑盒”测试用例的设计方法设计测试用例,对模块中重要的执行路径进行测试。重要的执行路径是通常指那些处在具体实现的算法、控制、数据处理等重要位置的路径,也可指较复杂而容易出错的路径。尽可能地对执行路径进行测试非常重要,需要设计因错误的计算、比较或控制流而导致错误的测试用例。此外,对基本执行路径和循环进行测试也可发现大量的路径错误。
在路径测试中,要检查的错误有:死代码、错误的计算优先级、算法错误、混用不同类的操作、初始化不正确、精度错误——比较运算错误、赋值错误、表达式的不正确符号——>、>=;=、==、!=和循环变量的使用错误——错误赋值以及其他错误等。
比较操作和控制流向紧密相关,测试用例设计需要注意发现比较操作的错误:
- 不同数据类型的比较(注意包装类与基础类型的比较)
- 不正确的逻辑运算符或优先次序
- 因浮点运算精度问题而造成的两值比较不等
- 关系表达式中不正确的变量和比较符
- “差1错”,即不正常的或不存在的循环中的条件
- 当遇到发散的循环时无法跳出循环
- 当遇到发散的迭代时不能终止循环
- 错误的修改循环变量
4.4 错误处理测试
错误处理路径是指可能出现错误的路径以及进行错误处理的路径。当出现错误时会执行错误处理代码,或通知用户处理,或停止执行并使程序进入一种安全等待状态。测试人员应意识到,每一行程序代码都可能执行到,不能自认为错误发生的概率很小而不进行测试。一般软件错误处理测试应考虑下面几种可能的错误:
- 出错的描述是否难以理解,是否能够对错误定位
- 显示的错误与实际的错误是否相符
- 对错误条件的处理正确与否
- 在对错误进行处理之前,错误条件是否已经引起系统的干预等
在进行错误处理测试时,要检查如下内容:
- 在资源使用前后或其他模块使用前后,程序是否进行错误出现检查
- 出现错误后,是否可以进行错误处理,如引发错误、通知用户、进行记录
- 在系统干预前,错误处理是否有效,报告和记录的错误是否真实详细
4.5 边界测试
边界测试是单元测试中最后的任务。代码常常在边界上出错,比如:在代码段中有一个n次循环,当到达第n次循环时就可能会出错;或者在一个有n个元素的数组中,访问第n个元素时是很容易出错的。因此,要特别注意数据流、控制流中刚好等于、大于或小于确定的比较值时可能会出现的错误。对这些地方需要仔细地认真加以测试。
此外,如果对模块性能有要求的话,还要专门对关键路径进行性能测试。以确定最坏情况下和平均意义下影响运行时间的因素。下面是边界测试的具体要检查的内容:
- 普通合法数据是否正确处理
- 普通非法数据是否正确处理
- 边界内最接近边界的(合法)数据是否正确处理
- 边界外最接近边界的(非法)数据是否正确处理等
- 在n次循环的第0次、第1次、第n次是否有错误
- 运算或判断中取最大最小值时是否有错误
- 数据流、控制流中刚好等于、大于、小于确定的比较值时是否出现错误
5 单元测试规范
5.1 命名规范
-
- 目录结构:Maven目录结构下的单元测试目录“test”
-
- 包名:被测试类包名
-
- 类名:被测试类名 + Test
- 方法名:test + 被测试方法名 + 4 + 测试内容(场景)
5.2 测试内容
第4部分概括的列举了需要测试的5大点内容,此处为服务端代码层至少要包含或覆盖的测试内容。
Service
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
HTTP接口
- 模拟接口测试
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
HSF接口
- 模拟接口测试
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
工具类
- 模拟接口测试
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
5.3 覆盖率
为了使单元测试能充分细致地展开,应在实施单元测试中遵守下述要求:
-
语句覆盖达到100%
语句覆盖指被测单元中每条可执行语句都被测试用例所覆盖。语句覆盖是强度最低的覆盖要求,要注重语句覆盖的意义。比如,用一段从没执行过的程序控制航天飞机升上天空,然后使它精确入轨,这种行为的后果不敢想象。实际测试中,不一定能做到每条语句都被执行到。第一,存在“死码”,即由于代码设计错误在任何情况下都不可能执行到的代码。第二,不是“死码”,但是由于要求的输入及条件非常难达到或单元测试的实现所限,使得代码没有得到执行。因此,在可执行语句未得到执行时,要深入程序作做详细的分析。如果是属于以上两种情况,则可以认为完成了覆盖。但是对于后者,也要尽量测试到。如果以上两者都不是,则是因为测试用例设计不充分,需要再设计测试用例。 -
分支覆盖达到100%
分支覆盖指分支语句取真值和取假值各一次。分支语句是程序控制流的重要处理语句,在不同流向上设计可以验证这些控制流向正确性的测试用命。分支覆盖使这些分支产生的输出都得到验证,提高测试的充分性。 -
覆盖错误处理路径
即异常处理路径 -
单元的软件特性覆盖
软件的特性包括功能、性能、属性、设计约束、状态数目、分支的行数等。 -
对试用额定数据值、奇异数据值和边界值的计算进行检验。用假想的数据类型和数据值运行测试,排斥不规则的输入。
单元测试通常是由编写程序的人自己完成的,但是项目负责人应当关心测试的结果。所有的测试用例和测试结果都是模块开发的重要资料,需妥善保存。
5.4 变异测试
测试覆盖方法的确可以帮我们找到一些显而易见的代码冗余或者测试遗漏的问题。不过,实践证明,这些传统的方法只能非常有限的发现测试中的问题。很多代码和测试的问题在覆盖达到100%的情况下也无法发现。然而,“代码变异测试”这种方法可以很好的弥补传统方法的缺点,产生更加有效的单元测试。
代码变异测试是通过对代码产生“变异”来帮助我们改进单元测试的。“变异”指的是修改一处代码来改变代码行为(当然保证语法的合理性)。简单来说,代码变异测试先试着对代码产生这样的变异,然后运行单元测试,并检查是否有测试是因为这个代码变异而失败。如果失败,那么说明这个变异被“消灭”了,这是我们期望看到的结果。否则说明这个变异“存活”了下来,这种情况下我们就需要去研究一下“为什么”了。
总而言之,测试覆盖这种方法是一种不错的保障单元测试质量的手段。代码变异测试则比传统的测试覆盖方法可以更加有效的发现代码和测试中潜在的问题,它可以使单元测试更加强壮。
6 CISE集成
省略
7 单元测试示例
7.1 Service
Service
层单元测试示例。
普通Mock测试:
2.错误(异常)处理:
3. Mock具体方法实现:
7.2 HTTP
HTTP
接口单元测试示例。
1. Spring MVC Controller
2. 参数化测试
7.3 工具类
静态工具类测试示例。
1. 静态方法:
或
附一:单元测试准则
1.保持单元测试小巧、快速
理论上,任何代码提交前都应该完整跑一遍所有测试套件。保持测试代码执行符合预期,这样能够缩短迭代开发周期。
2.单元测试应该是全自动/非交互式的
测试套件通常是定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。
3.让单元测试很容易跑起来
对开发环境进行配置,最好是敲一条命令或是点击一个按钮就能把单个测试用例或测试套件跑起来。
4.对测试进行评估
对执行的测试进行覆盖率分析,得到精确的代码执行覆盖率,并调查哪些代码未被执行。
5.立即修正失败的测试
每个开发人员在提交前都应该保证新的测试用例执行成功,当有代码提交时,现有测试用例也都能跑通。
如果一个定期执行的测试用例执行失败,整个团队应该放下手上的工作先解决这个问题。
6.把测试维持在单元级别
单元测试即类(Class)的测试。一个“测试类”应该只对应于一个“被测类”,并且“被测类”的行为应该被隔离测试。必须谨慎避免使用单元测试框架来测试整个程序的工作流,这样的测试即低效又难维护。工作流测试(译注:指跨模块、类的数据流测试)有它自己的地盘,但它绝不是单元测试,必须单独建立和执行。
7.由简入繁
再简单的测试也远远胜过完全没有测试。一个简单的“测试类”会促使建立“被测类”基本的测试骨架,可以对构建环境、单元测试环境、执行环境以及覆盖率分析工具等有效性进行检查,同时也可以证明“被测类”能够被整合和调用。
下面便是单元测试版的“Hello, world!”:
8.保持测试的独立性
为了保证测试稳定可靠且便于维护,测试用例之间决不能有相互依赖,也不能依赖执行的先后次序。(实际上TestNG有提供依赖的功能,或许某些场景也需要依赖。)
9.Keep tests close to the class being tested
[译注:有意翻译该规则,个人认为本条规则值得商榷,大部分C++、Objective-C和Python库均把测试代码从功能代码目录中独立出来,通常是创建一个和src目录同级的tests目录,被测模块、类名之前也常常不加Test 前缀。这么做保证功能代码和测试代码隔离、目录结构清晰,并且发布源码的时候更容易排除测试用例。]
If the class to test is Foo the test class should be called FooTest (not TestFoo) and kept in the same package (directory) as Foo. Keeping test classes in separate directory trees makes them harder to access and maintain.
Make sure the build environment is configured so that the test classes doesn’t make its way into production libraries or executables.
10.合理的命名测试用例
确保每个方法只测试“被测类”的一个明确特性,并相应的命名测试方法。典型的命名俗定是“test[MethodName]”,比如“testSaveAs()”、“testAddListener()”、“testDeleteProperty()”等。
11.只测公有接口
单元测试可以被定义为通过类的公有API对类进行测试。一些测试工具允许测试一个类的私有成员,但这种做法应该避免,它让测试变得繁琐而且更难维护。如果有私有成员确实需要进行直接测试,可以考虑把它重构到工具类的公有方法中。但要注意这么做是为了改善设计,而不是帮助测试。
12.看成是黑盒
站在第三方使用者的角度,测试一个类是否满足规定的需求。并设法让它出问题。
13.看成是白盒
毕竟被测试类是程序员自写自测的,应该在最复杂的逻辑部分多花些精力测试。
14.芝麻函数也要测试
通常建议所有重要的函数都应该被测试到,一些芝麻方法比如简单的setter和getter都可以忽略。但是仍然有充分的理由支持测试芝麻函数:
“芝麻”很难定义,对于不同的人有不同的理解。
从黑盒测试的观点看,是无法知道哪些代码是芝麻级别的。
即便是再芝麻的函数,也可能包含错误,通常是“复制粘贴”代码的后果:
因此建议测试所有方法,毕竟芝麻用例也容易测试。
15.先关注执行覆盖率
区别对待“执行覆盖率”和“实际测试覆盖率”。测试的最初目标应该是确保较高的执行覆盖率,这样能保证代码在 少量参数值输入时能执行成功。一旦执行覆盖率就绪,就应该开始改进测试覆盖率了。注意,实际的测试覆盖率很难衡量(而且往往趋近于0%)。
思考以下公有方法:
调用“setLength(1.0)”你可能会得到100%的执行覆盖率。但要达到100%的实际测试覆盖率,有多少个 double浮点数这个方法就必须被调用多少次,并且要一一验证行为的正确性。这无疑是不可能的任务。
16.覆盖边界值
确保参数边界值均被覆盖。对于数字,测试负数、0、正数、最小值、最大值、NaN(非数字)、无穷大等;对于字符串,测试空字符串、单字符、非ASCII字符串、多字节字符串等;对于集合类型,测试空、1、第一个、最后一个等;对于日期,测试1月1号、2月29号、12月31号等。被测试的类本身也会暗示一些特定情况下的边界值。 要点是尽可能彻底的测试这些边界值,因为它们都是主要“疑犯”。
17.提供一个随机值生成器
当边界值都覆盖了,另一个能进一步改善测试覆盖率的简单方法就是生成随机参数,这样每次执行测试都会有不同的输入。
想要做到这点,需要提供一个用来生成基本类型(如:浮点数、整型、字符串、日期等)随机值的工具类。生成器应该覆盖各种类型的所有取值范围。
如果测试时间比较短,可以考虑再裹上一层循环,覆盖尽可能多的输入组合。下面的例子是验证两次转换“little endian”和“big endian”字节序后是否返回原值。由于测试过程很快,可以让它跑上个一百万次。
18.每个特性只测一次
在测试模式下,有时会情不自禁的滥用断言。这种做法会导致维护更困难,需要极力避免。仅对测试方法名指示的特性进行明确测试。
因为对于一般性代码而言,保证测试代码尽可能少是一个重要目标。
19.使用显式断言
应该总是优先使用“assertEquals(a, b)”而不是“assertTrue(a == b)”,因为前者会给出更有意义的测试失败信息。在事先不确定输入值的情况下,这条规则尤为重要,比如之前使用随机参数值组合的例子。
20.提供反向测试
反向测试是指刻意编写问题代码,来验证鲁棒性和能否正确的处理错误。
假设如下方法的参数如果传进去的是负数,会立马抛出异常:
void setLength(double length) throws IllegalArgumentException
可以用下面的方法来测试这个特例是否被正确处理:
21.代码设计时谨记
编写和维护单元测试的代价是很高的,减少代码中的公有接口和循环复杂度是降低成本和使高覆盖率测试代码更易于编写和维护的有效方法。
一些建议:
22.使类成员常量化,在构造函数中进行初始化。减少setter方法的数量。
23.限制过度使用继承和公有虚函数。
24.通过使用友元类(C++)或包作用域(Java)来减少公有接口。
25.避免不必要的逻辑分支。
26.在逻辑分支中编写尽可能少的代码。
27.在公有和私有接口中尽量多用异常和断言验证参数参数的有效性。
28.限制使用快捷函数。对于黑箱而言,所有方法都必须一视同仁的进行测试。考虑以下简短的例子:
29.删除后者可以简化测试,但用户代码的工作量也将略微增加。
30.不要访问预设的外部资源
单元测试代码不应该假定外部的执行环境,以便在任何时候和任何地方都能执行。为了向测试提供必需的资源,这些资源应该由测试本身提供。
比如:一个解析某类型文件的类,可以把文件内容嵌入到测试代码里。在测试的时候写入到临时文件,测试结束再删除,而不是从预定的地址直接读取。
31.权衡测试成本
不写单元测试的代价很高,但是写单元测试的代价同样很高。要在这两者之间做适当的权衡,如果用执行覆盖率来衡量,业界标准通常在80%左右。
很典型的,读写外部资源的错误处理和异常处理就很难达到百分百的执行覆盖率。模拟数据库在事务处理到一半时发生故障并不是办不到,但相对于进行大范围的代码审查,代价可能太大了。
32.安排测试优先次序
单元测试是典型的自底向上过程,如果没有足够的资源测试一个系统的所有模块,就应该先把重点放在较底层的模块。
33.测试代码要考虑错误处理
考虑下面的这个例子:
如果第一个断言失败,后续语句会导致代码崩溃,剩下的测试都无法执行。任何时候都要为测试失败做好准备,避免单个失败的测试项中断整个测试套件的执行。上面的例子可以重写成:
34.写测试用例重现Bug
每上报一个Bug,都要写一个测试用例来重现这个Bug(即无法通过测试),并用它作为成功修正代码的检验标准。
35.了解局限
单元测试永远无法证明代码的正确性!!
一个跑失败的测试可能表明代码有错误,但一个跑成功的测试什么也证明不了。
单元测试最有效的使用场合是在一个较低的层级验证并文档化需求,以及回归测试:开发或重构代码,不会破坏已有功能的正确性。
附二:书籍推荐
《Google软件测试之道》
《单元测试的艺术》
《测试驱动开发》
《重构 改善既有代码的设计》
附三:参考资料
-
- 单元测试:
单元测试的定义、内容、步骤
单元测试的重要性及核心意义
详细讲解单元测试的内容
TestNG系列教程:并行执行测试
单元测试准则中文翻译
单元测试准则原文
-
- 测试覆盖:
-
- 测试驱动开发:
-
- TestNG:
JUnit 4与TestNG对比
TESTNG与JUNIT4对比
-
- JMockit: