8点了解Java服务端单元测试

一. 前言

单元测试并不仅是为了验证你当前所写的代码是否存在问题,更为重要的是它能够很大程度的保障往后因业务变动、修复Bug或重构等引发的代码变动而致使(或新增)的风险。css

同时将单元测试提早到编写正式代码进行(测试驱动开发),能够很好的提升对代码结构的设计。经过优先编写测试用例,能够很好的从用户角度来对功能的分解、使用过程和接口等进行设计,从而提升代码结构的高内聚、低耦合特性。使得对往后的需求变动或代码重构等更加高效、简洁。java

所以编写单元测试对产品开发和维护、技术提高和积累具备重大意义!面试

二. 第一个单元测试

首先写一个单元测试,这样有助于对后面内容的理解与实践。算法

2.1 开发环境

**IntelliJ IDEA **
IntelliJ IDEA默认自带并启用TestNG和覆盖率插件:spring

  • TestNG

在设置窗口查看TestNG插件是否安装与启用:apache

 

  • 覆盖率

一样,查看覆盖率插件能够搜索“Coverage”。IntelliJ IDEA的覆盖率统计工具备三种,JaCoCo、Emma和IntelliJ IDEA自带。api

 

  • 变异测试

一样,查看并安装变异测试插件能够搜索“PIT mutation testing”。数组


**Eclipse **
Eclipse须要自行安装单元测试相关插件:tomcat

  • 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

Junit4TestNGJava很是流行的单元测试框架。因TestNG更加简洁、灵活和功能丰富,因此咱们选用TestNG
下面经过与Junit4的比较来了解一下TestNG的特性:

注解支持

Junit4TestNG的注解对比:

特性 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工具备EasyMockjMockMockitoUnitils MockPowerMockJMockit等工具。
从这里能够看到,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 覆盖率

为了使单元测试能充分细致地展开,应在实施单元测试中遵照下述要求:

  1. 语句覆盖达到100%
    语句覆盖指被测单元中每条可执行语句都被测试用例所覆盖。语句覆盖是强度最低的覆盖要求,要注重语句覆盖的意义。好比,用一段从没执行过的程序控制航天飞机升上天空,而后使它精确入轨,这种行为的后果不敢想象。实际测试中,不必定能作到每条语句都被执行到。第一,存在“死码”,即因为代码设计错误在任何状况下都不可能执行到的代码。第二,不是“死码”,可是因为要求的输入及条件很是难达到或单元测试的实现所限,使得代码没有获得执行。所以,在可执行语句未获得执行时,要深刻程序做作详细的分析。若是是属于以上两种状况,则能够认为完成了覆盖。可是对于后者,也要尽可能测试到。若是以上二者都不是,则是由于测试用例设计不充分,须要再设计测试用例。

  2. 分支覆盖达到100%
    分支覆盖指分支语句取真值和取假值各一次。分支语句是程序控制流的重要处理语句,在不一样流向上设计能够验证这些控制流向正确性的测试用命。分支覆盖使这些分支产生的输出都获得验证,提升测试的充分性。

  3. 覆盖错误处理路径
    即异常处理路径

  4. 单元的软件特性覆盖
    软件的特性包括功能、性能、属性、设计约束、状态数目、分支的行数等。

  5. 对试用额定数据值、奇异数据值和边界值的计算进行检验。用假想的数据类型和数据值运行测试,排斥不规则的输入。

单元测试一般是由编写程序的人本身完成的,可是项目负责人应当关心测试的结果。全部的测试用例和测试结果都是模块开发的重要资料,需妥善保存。

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总结

单元测试永远没法证实代码的正确性!!
一个跑失败的测试可能代表代码有错误,但一个跑成功的测试什么也证实不了。
单元测试最有效的使用场合是在一个较低的层级验证并文档化需求,以及回归测试:开发或重构代码,不会破坏已有功能的正确性。

以上内容就是本篇的所有内容以上内容但愿对你有帮助,有被帮助到的朋友欢迎点赞,评论。若是对软件测试、接口测试、自动化测试、面试经验交流。感兴趣能够关注博主主页,会有同行一块儿技术交流哦。

相关文章
相关标签/搜索