8点了解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依赖
- TestNG
<dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>${testng.version}</version> <scope>test</scope> </dependency>
- JMockit
<dependency> <groupId>org.jmockit</groupId> <artifactId>jmockit</artifactId> <version>${jmockit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jmockit</groupId> <artifactId>jmockit-coverage</artifactId> <version>${jmockit.version}</version> <scope>test</scope> </dependency>
- Spring Test
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.kubek2k</groupId> <artifactId>springockito</artifactId> <version>${springockito.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.kubek2k</groupId> <artifactId>springockito-annotations</artifactId> <version>${springockito.version}</version> <scope>test</scope> </dependency>
- 其他(或许需要)
<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-servlet-api</artifactId> <version>${tomcat.servlet.api.version}</version> <scope>test</scope> </dependency>
2.3 创建单元测试
下面介绍通过IDE
自动创建单元测试的方法(也可手动完成):
IntelliJ IDEA
Eclipse:
2.在弹出的窗口中搜索“Test”,选择“TestNG class”后点击“Next”按钮:
3.在窗口中选择要创建的测试方法后点击“Next”按钮:
4.根据自己的情况设置包名、类名和Annotations等:
示例代码
可参考下例代码编写单元测试:
package org.light4j.unit.test; import mockit.Expectations; import mockit.Injectable; import mockit.Tested; import org.testng.Assert; import org.testng.annotations.Test; import wow.unit.test.remote.UserService; import java.util.List; /** * 单元测试demo * * @author jiazuo.ljz */ public class BookServiceTest { /** * 图书持久化类,远程接口 */ @Injectable private BookDAO bookDAO; /** * 用户服务,远程接口 */ @Injectable private UserService userService; /** * 图书服务,本地接口 */ @Tested(availableDuringSetup = true) private BookService bookService; /** * 测试根据用户的Nick查询用户的图书列表方法 * 其中“getUserBooksByUserNick”方法最终需要通过UserID查询DB, * 所以在调用此方法之前需要先对UserService类的getUserIDByNick方法进行Mock。 */ @Test public void testGetUserBooksByUserNick() throws Exception { new Expectations() { { userService.getUserIDByNick(anyString); // Mock接口 result = 1234567; // Mock接口的返回值 times = 1; // 此接口会被调用一次 } }; List<BookDO> bookList = bookService.getUserBooksByUserNick("moyuan.jcc"); Assert.assertNotNull(bookList); } }
2.4 运行单元测试
IntelliJ IDEA
Eclipse
注:也可点击工具栏选项运行,从左至右依次是:覆盖率、调试、运行运行。
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
Eclipse
2.输出报告
运行过程以及结果输出的窗口中有一行“JMockit: Coverage report written to”,是EclEmma创建的覆盖率报告文件目录:
覆盖率报告
2.6 变异测试
变异测试是覆盖率的一个很好的补充。相比覆盖率,它能够使单元测试更加健壮。(具体可见5.4节)
IntelliJ IDEA
3. 输出报告
运行过程以及结果输出的窗口中最后一行“Open report in browser”即为插件创建的报告连接。
点击即可打开报告:
Eclipse
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
@Test(expected = ArithmeticException.class) public void divisionWithException() { int i = 1/0; }
- TestNG
@Test(expectedExceptions = ArithmeticException.class) public void divisionWithException() { int i = 1/0; }
忽略测试
忽略测试是指这个单元测试可以被忽略。
- JUnit4
@Ignore("Not Ready to Run") @Test public void divisionWithException() { System.out.println("Method is not ready yet"); }
- TestNG
@Test(enabled=false) public void divisionWithException() { System.out.println("Method is not ready yet"); }
时间测试
时间测试是指一个单元测试运行的时间超过了指定时间(毫秒数),那么测试将失败。
- JUnit4
@Test(timeout = 1000) public void infinity() { while (true); }
- TestNG
@Test(timeOut = 1000) public void infinity() { while (true); }
套件测试
套件测试是指把多个单元测试组合成一个模块,然后统一运行。
- JUnit4
@RunWith
和@Suite
注解被用于执行套件测试。下面的代码是所展示的是在“JunitTest5
”被执行之后需要“JunitTest1
”和“JunitTest2
”也一起执行。所有的声明需要在类内部完成。
java
@RunWith(Suite.class) @Suite.SuiteClasses({JunitTest1.class, JunitTest2.class}) public class JunitTest5 {
- TestNG
是使用XML
配置文件来执行套件测试。下面的配置将“TestNGTest1
”和“TestNGTest2
”一起执行。
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > <suite name="My test suite"> <test name="testing"> <classes> <class name="com.fsecure.demo.testng.TestNGTest1" /> <class name="com.fsecure.demo.testng.TestNGTest2" /> </classes> </test> </suite>
TestNG
的另一种方式使用了组的概念,每个测试方法都可以根据功能特性分配到一个组里面。例如:
@Test(groups="method1") public void testingMethod1() { System.out.println("Method - testingMethod1()"); } @Test(groups="method2") public void testingMethod2() { System.out.println("Method - testingMethod2()"); } @Test(groups="method1") public void testingMethod1_1() { System.out.println("Method - testingMethod1_1()"); } @Test(groups="method4") public void testingMethod4() { System.out.println("Method - testingMethod4()"); }
这是一个有4个方法,3个组(method1, method2 和 method4)的类。使用起来比XML的套件更简洁。
下面XML
文件配置了一个执行组为methed1
的单元测试。
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > <suite name="My test suite"> <test name="testing"> <groups> <run> <include name="method1"/> </run> </groups> <classes> <class name="com.fsecure.demo.testng.TestNGTest5_2_0" /> </classes> </test> </suite>
分组使集成测试更加强大。例如,我们可以只是执行所有测试中的组名为DatabaseFuntion
的测试。
参数化测试
参数化测试是指给单元测试传多种参数值,验证接口对多种不同参数的处理是否正确。
- JUnit4
@RunWith
和@Parameter
注解用于为单元测试提供参数值,@Parameters
必须返回List
,参数将会被作为参数传给类的构造函数。
@RunWith(value = Parameterized.class) public class JunitTest6 { private int number; public JunitTest6(int number) { this.number = number; } @Parameters public static Collection<Object[]> data() { Object[][] data = new Object[][] { { 1 }, { 2 }, { 3 }, { 4 } }; return Arrays.asList(data); } @Test public void pushTest() { System.out.println("Parameterized Number is : " + number); } }
它的使用很不方便:一个方法的参数化测试必须定义一个测试类。测试参数通过一个注解为@Parameters且返回值为List参数值列表的静态方法。然后将方法返回值成员通过类的构造函数初始化为类的成员。最后再将类的成员做为参数去测试被测试方法。
- TestNG
使用XML
文件或@DataProvider注解两种方式为测试提供参数。
XML
文件配置参数化测试
方法上添加@Parameters注解,参数数据由TestNG的XML配置文件提供。这样做之后,我们可以使用不同的数据集甚至是不同的结果集来重用一个测试用例。另外,甚至是最终用户,QA或者QE可以提供他们自己的XML文件来做测试。
public class TestNGTest6_1_0 { @Test @Parameters(value="number") public void parameterIntTest(int number) { System.out.println("Parameterized Number is : " + number); } }
XML 文件
<!DOCTYPE suite SYSTEM "http://beust.com/testng/testng-1.0.dtd" > <suite name="My test suite"> <test name="testing"> <parameter name="number" value="2"/> <classes> <class name="com.fsecure.demo.testng.TestNGTest6_0" /> </classes> </test> </suite>
@DataProvider注解参数化测试
使用XML
文件初始化数据虽然方便,但仅支持基础数据类型。如需复杂的类型可使用@DataProvider
注解解决。
@Test(dataProvider = "Data-Provider-Function") public void parameterIntTest(Class clzz, String[] number) { System.out.println("Parameterized Number is : " + number[0]); System.out.println("Parameterized Number is : " + number[1]); } //This function will provide the patameter data @DataProvider(name = "Data-Provider-Function") public Object[][] parameterIntTestProvider() { return new Object[][]{ {Vector.class, new String[]{"java.util.AbstractList", "java.util.AbstractCollection"}}, {String.class, new String[] {"1", "2"}}, {Integer.class, new String[] {"1", "2"}} }; }
@DataProvider作为对象的参数
P.S “TestNGTest6_3_0” 是一个简单的对象,使用了get和set方法。
@Test(dataProvider = "Data-Provider-Function") public void parameterIntTest(TestNGTest6_3_0 clzz) { System.out.println("Parameterized Number is : " + clzz.getMsg()); System.out.println("Parameterized Number is : " + clzz.getNumber()); } //This function will provide the patameter data @DataProvider(name = "Data-Provider-Function") public Object[][] parameterIntTestProvider() { TestNGTest6_3_0 obj = new TestNGTest6_3_0(); obj.setMsg("Hello"); obj.setNumber(123); return new Object[][]{{obj}}; }
TestNG的参数化测试使用起来非常方便,它可以在一个测试类中添加多个方法的参数化测试(JUnit4一个方法就需要一个类)。
依赖测试
依赖测试是指测试的方法是有依赖的,在执行的测试之前需要执行的另一测试。如果依赖的测试出现错误,所有的子测试都被忽略,且不会被标记为失败。
- JUnit4
JUnit4框架主要聚焦于测试的隔离,暂时还不支持这个特性。
- TestNG
它使用dependOnMethods来实现了依赖测试的功能,如下:
@Test public void method1() { System.out.println("This is method 1"); } @Test(dependsOnMethods={"method1"}) public void method2() { System.out.println("This is method 2"); }
如果method1()成功执行,那么method2()也将被执行,否则method2()将会被忽略。
性能测试
TestNG
支持通过多个线程并发调用一个测试接口来实现性能测试。JUnit4
不支持,若要进行性能测试需手动添加并发代码。
@Test(invocationCount=1000, threadPoolSize=5, timeOut=100) public void perfMethod() { System.out.println("This is perfMethod"); }
并行测试
TestNG
支持通过多个线程并发调用多个测试接口执行测试,相对于传统的单线程执行测试的方式,可以很大程度减少测试运行时间。
public class ConcurrencyTest { @Test public void method1() { System.out.println("This is method 1"); } @Test public void method2() { System.out.println("This is method 2"); } }
并行测试配置:
<suite name="Concurrency Suite" parallel="methods" thread-count="2" > <test name="Concurrency Test" group-by-instances="true"> <classes> <class name="wow.unit.test.ConcurrencyTest" /> </classes> </test> </suite>
讨论总结
通过上面的对比,建议使用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
等工具。
从这里可以看到,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”方法执行前被创建出来。
@Tested(availableDuringSetup = true) private BookService bookService;
- Mock对象
@Injectable:JMockit自动创建注解为“@Injectable”的类对象,并将其自动注入被测试对象。
@Injectable private BookDAO bookDAO; @Injectable private UserService userService;
相关的注解还有:// TODO 待补充
- 录制
Expectations:块里的内容是用来Mock方法,并指定方法的返回值、异常、调用次数和耗时。此块中的方法是必须被执行的,否则单元测试失败。
/** * 测试根据用户的Nick查询用户的图书列表方法 * 其中“getUserBooksByUserNick”方法最终需要通过UserId查询DB, * 所以在调用此方法之前需要先对UserService类的getUserIdByNick方法进行Mock。 */ @Test public void testGetUserBooksByUserNick() throws Exception { new Expectations() { { userService.getUserIdByNick(anyString); result = 1234567; times = 1; } }; List<BookDO> bookList = bookService.getUserBooksByUserNick("moyuan.jcc"); Assert.assertNotNull(bookList); }
相关的类还有:
- 结果验证
Assert:是最常见的断言验证
Assert.assertNotNull(bookList);
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 命名规范
5.2 测试内容
第4部分概括的列举了需要测试的5大点内容,此处为服务端代码层至少要包含或覆盖的测试内容。
Service
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
HTTP接口
- 模拟接口测试
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
HSF接口
- 模拟接口测试
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
工具类
- 模拟接口测试
- 局部数据结构测试
- 路径测试
- 错误处理测试
- 边界测试
5.3 覆盖率
为了使单元测试能充分细致地展开,应在实施单元测试中遵守下述要求:
-
语句覆盖达到100%
语句覆盖指被测单元中每条可执行语句都被测试用例所覆盖。语句覆盖是强度最低的覆盖要求,要注重语句覆盖的意义。比如,用一段从没执行过的程序控制航天飞机升上天空,然后使它精确入轨,这种行为的后果不敢想象。实际测试中,不一定能做到每条语句都被执行到。第一,存在“死码”,即由于代码设计错误在任何情况下都不可能执行到的代码。第二,不是“死码”,但是由于要求的输入及条件非常难达到或单元测试的实现所限,使得代码没有得到执行。因此,在可执行语句未得到执行时,要深入程序作做详细的分析。如果是属于以上两种情况,则可以认为完成了覆盖。但是对于后者,也要尽量测试到。如果以上两者都不是,则是因为测试用例设计不充分,需要再设计测试用例。 -
分支覆盖达到100%
分支覆盖指分支语句取真值和取假值各一次。分支语句是程序控制流的重要处理语句,在不同流向上设计可以验证这些控制流向正确性的测试用命。分支覆盖使这些分支产生的输出都得到验证,提高测试的充分性。 -
覆盖错误处理路径
即异常处理路径 -
单元的软件特性覆盖
软件的特性包括功能、性能、属性、设计约束、状态数目、分支的行数等。 -
对试用额定数据值、奇异数据值和边界值的计算进行检验。用假想的数据类型和数据值运行测试,排斥不规则的输入。
单元测试通常是由编写程序的人自己完成的,但是项目负责人应当关心测试的结果。所有的测试用例和测试结果都是模块开发的重要资料,需妥善保存。
5.4 变异测试
测试覆盖方法的确可以帮我们找到一些显而易见的代码冗余或者测试遗漏的问题。不过,实践证明,这些传统的方法只能非常有限的发现测试中的问题。很多代码和测试的问题在覆盖达到100%的情况下也无法发现。然而,“代码变异测试”这种方法可以很好的弥补传统方法的缺点,产生更加有效的单元测试。
代码变异测试是通过对代码产生“变异”来帮助我们改进单元测试的。“变异”指的是修改一处代码来改变代码行为(当然保证语法的合理性)。简单来说,代码变异测试先试着对代码产生这样的变异,然后运行单元测试,并检查是否有测试是因为这个代码变异而失败。如果失败,那么说明这个变异被“消灭”了,这是我们期望看到的结果。否则说明这个变异“存活”了下来,这种情况下我们就需要去研究一下“为什么”了。
总而言之,测试覆盖这种方法是一种不错的保障单元测试质量的手段。代码变异测试则比传统的测试覆盖方法可以更加有效的发现代码和测试中潜在的问题,它可以使单元测试更加强壮。
6 CISE集成
7 单元测试示例
7.1 Service
Service
层单元测试示例。
普通Mock测试:
/** * 测试根据用户的Nick查询用户的图书列表方法 * 其中“userService.getUserBooksByUserNick”方法最终需要通过UserId查询DB, * 所以在调用此方法之前需要先对UserService类的getUserIdByNick方法进行Mock。 * 其中“bookDAO.getUserBooksByUserId”方法最终需要通过UserId查询DB, * 所以在调用此方法之前需要先对BookDAO类的getUserBooksByUserId方法进行Mock。 */ @Test public void testGetUserBooksByUserNick4Success() throws Exception { final List<BookDO> bookList = new ArrayList<BookDO>(); bookList.add(new BookDO()); new Expectations() { { userService.getUserIdByNick(anyString); // Mock的接口 result = 1234567; // 接口返回值 times = 1; // 接口被调用的次数 bookDAO.getUserBooksByUserId(anyLong); result = bookList; times = 1; } }; List<BookDO> resultBookList = bookService.getUserBooksByUserNick("moyuan.jcc"); Assert.assertNotNull(resultBookList); }
2.错误(异常)处理:
/** * 测试根据用户的Nick查询用户的图书列表方法,注意在@Test添加expectedExceptions参数 * 验证其中“userService.getUserBooksByUserNick”接口出现异常时,对异常的处理是否符合预期. * 其中“bookDAO.getUserBooksByUserId”方法不会被调用到。 */ @Test(expectedExceptions = {RuntimeException.class}) public void testGetUserBooksByUserNick4Exception() throws Exception { final List<BookDO> bookList = new ArrayList<BookDO>(); bookList.add(new BookDO()); new Expectations() { { userService.getUserIdByNick(anyString); // Mock的接口 result = new RuntimeException("exception unit test"); // 接口抛出异常 times = 1; // 接口被调用的次数 bookDAO.getUserBooksByUserId(anyLong); result = bookList; times = 0; // 上面接口出现异常后,此接口不会被调用 } }; List<BookDO> resultBookList = bookService.getUserBooksByUserNick("moyuan.jcc"); Assert.assertNotNull(resultBookList); }
3. Mock具体方法实现:
/** * 测试发送离线消息方法 * 消息队列:当离线消息超过100条时,删除最旧1条,添加最新一条。 * 但消息存在DB或Tair中,所以需要Mock消息的存储。 */ @Test public void testAddOffLineMsg() throws Exception { final Map<Long, MsgDO> msgCache = new ArrayList<Long, MsgDO>(); new Expectations() { { new MockUp<BookDAO>() { @Mock public void addMsgByUserId(long userId, MsgDO msgDO) { msgCache.put(userId, msgDO); } }; new MockUp<BookDAO>() { @Mock public List<MsgDO> getUserBooksByUserId(long userId) { return msgCache.get(userId); } }; } }; final int testAddMsgCount = 102; for(int i = 0; i < testAddMsgCount; i++) { msgService.addMsgByUserId(123L, new MsgDO(new Date(), "this is msg" + i)); } List<MsgDO> msgList = msgService.getMsgByUserId(123L); Assert.assertTrue(msgList.size() == 100); new Verifications() { { // 验证 addMsgByUserId 接口是否被调用了100次 MsgDAO.addMsgByUserId(anyLong, withInstanceOf(MsgDO.class)); times = testAddMsgCount; // 验证是否对消息内容进行相就次数的转义 SecurityUtil.escapeHtml(anyString); times = testAddMsgCount; } }; }
7.2 HTTP
HTTP
接口单元测试示例。
1. Spring MVC Controller
public final class BookControllerTest { @Tested(availableDuringSetup = true) private BookController bookController; @Injectable private BookService bookService; private MockMvc mockMvc; @BeforeMethod public void setUp() throws Exception { this.mockMvc = MockMvcBuilders.standaloneSetup(bookController).build(); } /** *<strong> </strong>******************************** * getBookList unit test *<strong> </strong>******************************** */ @Test public void testgetBookList4Success() throws Exception { new StrictExpectations() { { new MockUp<CookieUtil>(){ @Mock public boolean isLogined(){ return true; } }; userService.getUserBooksByUserNick(anyString); result = null; times = 1; } }; ResultActions httpResult = this.mockMvc.perform(get("/education/getBookList.do?userNick=hello")) .andDo(print()).andExpect(status().isOk()); MockHttpServletResponse response = httpResult.andReturn().getResponse(); String responseStr = response.getContentAsString(); // 如果存在多版本客户端的情况下,注意返回值向后兼容,此处需要多种格式验证. Assert.assertEquals(responseStr, "{\"code\":1,\"msg\":\"success\",\"data\":\"\"}"); } }
2. 参数化测试
@DataProvider(name = "getBookListParameterProvider") public Object[][] getBookListParameterProvider() { return new String[][]{ {"hello", "{\"code\":1,\"msg\":\"success\",\"data\":\"\"}"}, {"123", "{\"code\":301,\"msg\":\"parameter error\",\"data\":\"\"}"} }; } @Test(dataProvider = "getBookListParameterProvider") public void testgetBookList4Success(String nick ,String resultCheck) throws Exception { new StrictExpectations() { { new MockUp<CookieUtil>() { @Mock public boolean isLogined() { return true; } }; userService.getUserBooksByUserNick(anyString); result = null; times = 1; } }; ResultActions httpResult = this.mockMvc.perform(get("/education/getBookList.do?userNick=" + nick)) .andDo(print()).andExpect(status().isOk()); MockHttpServletResponse response = httpResult.andReturn().getResponse(); String responseStr = response.getContentAsString(); // 如果存在多版本客户端的情况下,注意返回值向后兼容,此处需要多种格式验证. Assert.assertEquals(responseStr, resultCheck); }
7.3 工具类
静态工具类测试示例。
1. 静态方法:
java @Test public void testMethod() { new StrictExpectations(CookieUtil) { { CookieUtil.isLogined(); result =
或
java @Test public void testMethod() { new MockUp<CookieUtil>(){ @Mock public boolean isLogined(){ return true;
8总结
单元测试永远无法证明代码的正确性!!
一个跑失败的测试可能表明代码有错误,但一个跑成功的测试什么也证明不了。
单元测试最有效的使用场合是在一个较低的层级验证并文档化需求,以及回归测试:开发或重构代码,不会破坏已有功能的正确性。
以上内容就是本篇的全部内容以上内容希望对你有帮助,有被帮助到的朋友欢迎点赞,评论。
如果对软件测试、接口测试、自动化测试、面试经验交流。感兴趣可以关注博主主页,会有同行一起技术交流哦。