基于TestNG和PowerMock的单元测试指南

什么是TestNG

TestNG是一套开源测试框架,是从JUnit继承而来,TestNG意为test next generation。它的优势如下:

  • 支持注解。
  • 可以在任意的大线程池中,使用各种策略运行测试(所有方法都可以拥有自己的线程或者每个测试类拥有一个线程等等)。
  • 代码多线程安全测试。
  • 灵活的测试配置。
  • 支持数据驱动测试(@DataProvider)。
  • 支持参数。
  • 强大的执行模型(不再用TestSuite)。
  • 支持各种工具和插件(Eclipse、IDEA、Maven等)。
  • 可以更灵活地嵌入BeanShell。
  • 默认JDK运行时功能和日志记录(无依赖关系)。
  • 依赖应用服务测试的方式。

TestNG的最简单示例

我们先来看一个TestNG最简单的示例:

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.testng.annotations.Test;

public class HelloWorld {

@BeforeClass
public void beforeClass() {
System.out.println("this is before class");
}

@Test
public void TestHelloWorld() {
System.out.println("this is HelloWorld test case");
}

@AfterClass
public void afterClass() {
System.out.println("this is after class");
}
}

从面上的简单示例中可以看出TestNG的生命周期,并且可以看到TestNG是使用注解来控制生命周期的。下面我们来看看TestNG支持的各种注解。

TestNG的注解

TestNG的注解大部分用在方法级别上,一共有六大类注解。

Before类别和After类别注解

  • @BeforeSuite:被注解的方法将会在所有测试类执行之前运行。
  • @AfterSuite:被注解的方法将会在所有测试类执行之后运行。
  • @BeforeTest:被注解的方法将会在当前测试类中的所有测试方法执行之前运行。
  • @AfterTest:被注解的方法将会在当前测试类中的所有测试方法执行之后运行。
  • @BeforeClass:被注解的方法将会在当前测试类中的第一个测试方法执行之前运行。
  • @AfterClass:被注解的方法将会在当前测试类中的最后一个测试方法执行之后运行。
  • @BeforeMethod:被注解的方法将会在当前测试类中的每个测试方法执行之前运行。
  • @AfterMethod:被注解的方法将会在当前测试类中的每个测试方法执行之后运行。
    我们可以根据不同的场景来使用不同的注解。

@Test注解

@Test注解是TestNG的核心注解,被打上该注解的方法,表示为一个测试方法,这个注解有多个配置属性,用法如下所示:

@Test(param1 = ..., param2 = ...)
  • alwaysRun:如果=true,表示即使该测试方法所依赖的前置测试有失败的情况,也要执行。
  • dataProvider:选定传入参数的构造器。(后面会讲到@DataProvider注解)
  • dataProviderClass:确定参数构造器的Class类。(参数构造器首先会在当前测试类里面查找,如果参数构造器不在当前测试类定义,那么必须使用该属性来执行它所在的Class类)
  • dependsOnGroups:确定依赖的前置测试组别。
  • dependsOnMethods:确定依赖的前置测试方法。
  • description:测试方法描述信息。(建议为每个测试方法添加有意义的描述信息,这将会在最后的报告中展示出来)
  • enabled:默认为true,如果指定为false,表示不执行该测试方法。
  • expectedExceptions:指定期待测试方法抛出的异常,多个异常以逗号隔开。
  • groups:指定该测试方法所属的组,可以指定多个组,以逗号隔开。
  • invocationCount:指定测试方法需要被调用的次数。
  • invocationTimeOut:每一次调用的超时时间,如果invocationCount没有指定,该参数会被忽略。应用场景可以为测试获取数据库连接,超时就认定为失败。单位是毫秒。
  • priority:指定测试方法的优先级,数值越低,优先级越高,将会优先与其他数值高的测试方法被调用。(注意是针对一个测试类的优先级)
  • timeout:指定整个测试方法的超时时间。单位是毫秒。

下面我们写一个简单的测试类,说明@Test注解的使用以及属性的配置方式:

public class TestAnnotationPropertiesTest {

@Test(priority = 1, invocationCount = 3)
public void test1() {
System.out.println("invoke test1");
}

@Test(priority = 2, invocationCount = 2)
public void test2() {
System.out.println("invoke test2");
}

}

testng.xml:

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite1" verbose="1" >
<test name="test1" >
<classes>
<class name="com.testngdemo.TestAnnotationPropertiesTest" />
</classes>
</test>
</suite>

测试结果:

invoke test1
invoke test1
invoke test1
invoke test2
invoke test2

@Parameters注解

@Parameters 注解用于为测试方法传递参数, 用法如下所示:

public class AnnotationParametersTest {

@Parameters(value = {"param1", "param2"})
@Test
public void test(String arg1, String arg2) {
System.out.println("use @Parameters to fill method arguments : arg 1 = " + arg1 + ", arg2 = " + arg2);
}

}

testng.xml配置:

<test name="testAnnotationParameters">
<parameter name="param1" value="value1"></parameter>
<parameter name="param2" value="value2"></parameter>
<classes>
<class name="com.testngdemo.AnnotationParametersTest" />
</classes>
</test>

测试结果:

use @Parameters to fill method arguments : arg 1 = value1, arg2 = value2

@DataProvider注解

上面提到@Parameters注解可以为测试方法传递参数,但是这种方式参数值需要配置在testng.xml里面,灵活性不高。而@DataProvider注解同样可以为测试方法传递参数值,并且,它是真正意义上的参数构造器,可以传入多组测试数据对测试方法进行测试。被@DataProvider注解的方法,方法返回值必须为Object[][]或者Iterator,用法如下所示:

public class AnnotationDataProviderTest {

@DataProvider(name="testMethodDataProvider")
public Object[][] testMethodDataProvider() {
return new Object[][]{{"value1-1", "value2-1"}, {"value1-2", "value2-2"}, {"value1-3", "value2-3"}};
}

@Test(dataProvider="testMethodDataProvider")
public void test(String arg1, String arg2) {
System.out.println("use @DataProvider to fill method argument : arg1 = " + arg1 + " , arg2 = " + arg2);
}

}

testng.xml:

<test name="testDataProvider">
<classes>
<class name="com.testngdemo.AnnotationDataProviderTest" />
</classes>
</test>

测试结果:

use @DataProvider to fill method argument : arg1 = value1-1 , arg2 = value2-1
use @DataProvider to fill method argument : arg1 = value1-2 , arg2 = value2-2
use @DataProvider to fill method argument : arg1 = value1-3 , arg2 = value2-3

@Factory 注解

在一个方法上面打上@Factory注解,表示该方法将返回能够被TestNG测试的测试类。利用了设计模式中的工厂模式,用法如下所示:

public class AnnotationFactoryTest {

@Factory
public Object[] getSimpleTest() {
return new Object[]{ new SimpleTest("one"), new SimpleTest("two")};
}

}
public class SimpleTest {

private String param;

public SimpleTest(String param) {
this.param = param;
}

@Test
public void test() {
System.out.println("SimpleTest.param = " + param);
}
}

testng.xml配置:

<test name="testFactory">
<classes>
<class name="com.crazypig.testngdemo.AnnotationFactoryTest" />
</classes>
</test>

测试结果:

SimpleTest.param = one
SimpleTest.param = two

@Listeners 注解

一般我们写测试类不会涉及到这种类型的注解,这个注解必须定义在类、接口或者枚举类级别。实用的Listener包括ISuiteListener、ITestListener和IInvokedMethodListener,他们可以在suite级别、test级别和test method一些执行点执行一些自定义操作,如打印日志等。

TestNG的配置文件

在上文的示例中,出现了testng.xml这种东西,这一节就来看看TestNG的配置文件。和JUnit一样,TestNG也可以直接在测试类中针对某个测试方法运行,或者运行整个测试类。此外TestNG还提供了更强大和灵活的配置文件方式,比如在配置文件中控制测试类的执行策略,是并行还是串行,在配置文件里进行传参以及对测试类进行分组测试等。这样能让运行测试方法和测试类更有逻辑性,能灵活的应对不同的测试场景,最大化的覆盖业务场景。

当下,Java开发基本都使用Maven来进行依赖管理,TestNG也同样支持Maven,这样我们只需要在POM文件中将TestNG的配置文件配置进去,就可以使用mvn test来跑单元测试了。示例如下:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.16</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>

testng.xml的基本格式

标签

元素是testng.xml文件的根元素。从DTD文件(如下所示)可以看出,可以包含一个元素,用以定义全局的组,该组对所有的测试可见。可以包含多个元素,一个就定义了一个测试用例(其中可能包含多个测试方法)。

<!ELEMENT suite (groups?,(listeners|packages|test|parameter|method-selectors|suite-files)*) >

示例如下:

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite1" verbose="1" >
<groups>
<run>
<include name="..." />
<exclude name="..." />
</run>
</groups>

<test name="HelloWorld1">
...
</test>
</suite>

标签中有一个重要的属性是parallel,通过该属性可以配置测试用例的线程运行策略,该属性的具体值如下:

  • methods:针对每个测试方法启独立线程运行。
  • classes:针对每个测试类启独立线程运行,该测试类中的所有测试方法均在一个线程中运行。
  • instances:针对测试类实例启独立线程运行,不同实例的相同测试方法在不同的线程中运行。

标签

元素是的子元素,用以定义一个测试用例。定义测试用例可以通过

<!ELEMENT test (method-selectors?,parameter*,groups?,packages?,classes?) >
  • 表示以测试类的方式定义测试用例,粒度较小。示例如下:
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite1" verbose="1" >
<test name="HelloWorld1">
<classes>
<class name="test.sample.ParameterSample"/>
<class name="test.sample.ParameterTest"/>
</classes>
</test>
</suite>
  • 表示以测试类所在的包的方式定义测试用例,包中的所有测试类都被涉及,粒度较大。示例如下:
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Suite1" verbose="1" >
<test name="HelloWorld1" >
<packages>
<package name="test.sample" />
</packages>
</test>
</suite>
  • 元素中也有元素,上文中提到,中可以定义一个全局的。而这里元素中也可以定义一个自己的,其中定义的组仅对当前所在的测试用例可见。示例如下:
<test name="HelloWorld1">
<groups>
<run>
<exclude name="brokenTests" />
<include name="checkinTests" />
</run>
</groups>

<classes>
...
</classes>
</test>
  • 元素可以用于在配置文件中给测试方法传递参数,在上文中的@Parameters注解一节中有示例。

注意:在testng.xml配置文件中,中可以定义多个,那么这些的执行顺序默认按照其在中出现的先后顺序。当然,也可以提供的preserve-order=’false’改变默认顺序。

标签

可以通过定义测试用例,但只是在测试类或类包的层次上,那么能不能具体到测试类的某个方法呢?
对于中的一个,可以提供设置测试方法。示例如下:

<test name="HelloWorld1">
<classes>

<class name="test.Test1">
<methods>
<include name="m1" />
<include name="m2" />
</methods>
</class>

<class name="test.Test2" />

</classes>
</test>

什么是PowerMock

mock是模拟对象,用于模拟真实对象的行为。PowerMock可以支持EasyMock和Mockito,作为Mockito的扩展,使用PowerMock可以mock private方法,mock static方法,mock final方法,mock construction方法。PowerMock封装了部分Mockito的API,可以使用Mockito的语法来进行测试代码的编写。

PowerMock注解@PrepareForTest

@PrepareForTest(Employee.class)语句告诉PowerMock准备Employee类进行测试。适用于模拟final类或有final, private, static, native方法的类 @PrepareForTest是当使用PowerMock强大的Mock静态、final、private方法时,需要添加的注解。 如果测试用例里没有使用注解@PrepareForTest,可以不加注解@RunWith(PowerMockRunner.class),反之亦然。

PowerMockTestCase父类

如果想在TestNG框架下使用PowerMock,那么需要测试类继承PowerMockTestCase类,作用是告诉TestNG框架,在测试方法中会用到PowerMock的功能,比如mock static、final、private方法等。

测试static方法

示例中的待测试类为Employee:

public class Employee {
public static int count() {
throw new UnsupportedOperationException();
}
public int getEmployeeCount() {
return Employee.count();
}
}

对应的测试类为EmployeeServiceTest:

@PrepareForTest(Employee.class)
public class EmployeeServiceTest extends PowerMockTestCase {
@Test
public void shouldReturnTheCountOfEmployeesUsingTheDomainClass() {

PowerMockito.mockStatic(Employee.class);
PowerMockito.when(Employee.count()).thenReturn(900);

EmployeeService employeeService = new EmployeeService();
assertEquals(900, employeeService.getEmployeeCount());

}
}

从上面的示例中可以看到,如果要mock静态方法,需要使用mockStatic()方法来mock该静态方法所属的类,然后通过PowerMock的链式语法对方法进行mock,即当调用某个方法时,我期望返回什么值,所以PowerMockito.when(Employee.count()).thenReturn(900);这句用大白话来翻译的话就是当调用Employee的count()方法时期望返回900。然后再调用要测试的方法,使用断言对结果进行验证。

测试返回void的静态方法

示例中的待测试类为EmployeeService:

public class Employee {
public static void giveIncrementOf(int percentage) {
throw new UnsupportedOperationException();
}
}

public class EmployeeService {
public boolean giveIncrementToAllEmployeesOf(int percentage) {
try{
Employee.giveIncrementOf(percentage);
return true;
} catch(Exception e) {
return false;
}
}
}

对应的测试类为EmployeeServiceTest:

@PrepareForTest(Employee.class)
public class EmployeeServiceTest extends PowerMockTestCase {
@Test
public void shouldReturnTrueWhenIncrementOf10PercentageIsGivenSuccessfully() {
PowerMockito.mockStatic(Employee.class);
PowerMockito.doNothing().when(Employee.class);
Employee.giveIncrementOf(10);
EmployeeService employeeService = new EmployeeService();
assertTrue(employeeService.giveIncrementToAllEmployeesOf(10));
}

@Test
public void shouldReturnFalseWhenIncrementOf10PercentageIsNotGivenSuccessfully() {
PowerMockito.mockStatic(Employee.class);
PowerMockito.doThrow(new IllegalStateException()).when(Employee.class);
Employee.giveIncrementOf(10);
EmployeeService employeeService = new EmployeeService();
assertFalse(employeeService.giveIncrementToAllEmployeesOf(10));
}
}

这个示例中展示了调用方法不触发实现逻辑及抛异常的PowerMock方法doNothing()doThrow()

  • doNothing():该方法告诉PowerMock下一个方法调用时什么也不做。
  • doThrow():该方法告诉PowerMock下一个方法调用时产生给定异常。

验证方法是否调用

示例中的待测试类为EmployeeService:

public class EmployeeService{
public void saveEmployee(Employee employee) {
if(employee.isNew()) {
employee.create();
return;
}
employee.update();
}
}

public class Employee{
public boolean isNew() {
throw new UnsupportedOperationException();
}

public void update() {
throw new UnsupportedOperationException();
}

public void create() {
throw new UnsupportedOperationException();
}

public static void giveIncrementOf(int percentage) {
throw new UnsupportedOperationException();
}
}

对应的测试类为EmployeeServiceTest:

public class EmployeeServiceTest extends PowerMockTestCase {
@Test
public void shouldCreateNewEmployeeIfEmployeeIsNew() {

Employee mock = PowerMockito.mock(Employee.class);
PowerMockito.when(mock.isNew()).thenReturn(true);
EmployeeService employeeService = new EmployeeService();
employeeService.saveEmployee(mock);
Mockito.verify(mock).create();
Mockito.verify(mock, Mockito.never()).update();
}
}

从上面的示例中看到,首先使用了PowerMockito.mock()而不是PowerMockito.mockStaic(),因为我们要测试的不是静态方法。Mockito.verify(mock).create()验证调用了create()方法。 Mockito.verify(mock, Mockito.never()).update()验证没有调用update()方法。

我们再来看看如何验证静态方法:

public class Employee {
public static void giveIncrementOf(int percentage) {
throw new UnsupportedOperationException();
}
}

public class EmployeeService{
public boolean giveIncrementToAllEmployeesOf(int percentage) {
try{
Employee.giveIncrementOf(percentage);
return true;
} catch(Exception e) {
return false;
}
}
}
public class EmployeeServiceTest extends PowerMockTestCase {
@Test
public void shouldInvoke_giveIncrementOfMethodOnEmployeeWhileGivingIncrement() {
PowerMockito.mockStatic(Employee.class);
PowerMockito.doNothing().when(Employee.class);
Employee.giveIncrementOf(9);
EmployeeService employeeService = new EmployeeService();
employeeService.giveIncrementToAllEmployeesOf(9);
PowerMockito.verifyStatic();
}
}

首先还是使用PowerMockito.mockStatic()方法进行mock,然后使用PowerMockito.verifyStatic()验证静态方法是否调用。

验证方法的调用次数及调用顺序

示例中的待测试类为EmployeeService:

public class EmployeeService{
public void saveEmployee(Employee employee) {
if(employee.isNew()) {
employee.create();
return;
}
employee.update();
}
}

public class Employee{
public boolean isNew() {
throw new UnsupportedOperationException();
}

public void update() {
throw new UnsupportedOperationException();
}

public void create() {
throw new UnsupportedOperationException();
}

public static void giveIncrementOf(int percentage) {
throw new UnsupportedOperationException();
}
}

对应的测试类为EmployeeServiceTest:

@Test
public void shouldInvokeIsNewBeforeInvokingCreate() {
Employee mock = PowerMockito.mock(Employee.class);
PowerMockito.when(mock.isNew()).thenReturn(true);
EmployeeService employeeService = new EmployeeService();
employeeService.saveEmployee(mock);
InOrder inOrder = Mockito.inOrder(mock);
inOrder.verify(mock).isNew();
inOrder.verify(mock).create();
Mockito.verify(mock, Mockito.never()).update();
Mockito.verify(mock, Mockito.times(1)).isNew();
}

上面的示例中,通过Mockito.inOrder()方法通过mock的对象构造了InOrder对象,然后按期望的顺序验证方法的调用顺序。Mockito.verify(mock, Mockito.times(1)).isNew()这句的意思是验证Employee类的isNew()方法调用了一次,除此之外,还能验证一些其他的调用次数策略:

  • Mockito.times(int n): 准确的验证方法调用的次数。
  • Mockito.atLeastOnce(): 验证方法至少调用一次 。
  • Mockito.atLeast(int n): 验证方法最少调用次数 。
  • Mockito.atMost(int n): 验证方法最多调用次数。

测试final类或方法

示例中的待测试类为EmployeeGenerator

public final class EmployeeIdGenerator {

public final static int getNextId() {
throw new UnsupportedOperationException();
}
}
public class Employee {
public void setEmployeeId(int nextId) {

throw new UnsupportedOperationException();
}
}
public class EmployeeService {
public void saveEmployee(Employee employee) {
if(employee.isNew()) {
employee.setEmployeeId(EmployeeIdGenerator.getNextId());
employee.create();
return;
}
employee.update();
}
}

测试类为EmployeeServiceTest

@PrepareForTest(EmployeeIdGenerator.class)
public class EmployeeServiceTest extends PowerMockTestCase {

@Test
public void shouldGenerateEmployeeIdIfEmployeeIsNew() {

Employee mock = PowerMockito.mock(Employee.class);
PowerMockito.when(mock.isNew()).thenReturn(true);
PowerMockito.mockStatic(EmployeeIdGenerator.class);
PowerMockito.when(EmployeeIdGenerator.getNextId()).thenReturn(90);
EmployeeService employeeService = new
EmployeeService();
employeeService.saveEmployee(mock);
PowerMockito.verifyStatic();
EmployeeIdGenerator.getNextId();
Mockito.verify(mock).setEmployeeId(90);
Mockito.verify(mock).create();
}
}

从示例中看到,测试方法体中并没有什么特殊的存在,只是在EmployeeServiceTest类上使用了@PrepareForTest注解来申明将要用到PowerMock的能力。

测试构造方法

示例中的待测试类为WelcomeEmail

public class WelcomeEmail {

public WelcomeEmail(final Employee employee, final String message) {
throw new UnsupportedOperationException();
}

public void send() {
throw new UnsupportedOperationException();
}
}
public class EmployeeService{
public void saveEmployee(Employee employee) {
if(employee.isNew()) {
employee.setEmployeeId(EmployeeIdGenerator.getNextId());
employee.create();
WelcomeEmail emailSender = new WelcomeEmail(employee,
"Welcome to Mocking with PowerMock How-to!");
emailSender.send();
return;
}
employee.update();
}
}

测试类为WelcomeEmailTest

public class  WelcomeEmailTest {
@PrepareForTest({EmployeeIdGenerator.class, EmployeeService.class})
public class EmployeeServiceTest extends PowerMockTestCase {

@Test
public void shouldSendWelcomeEmailToNewEmployees()throws Exception {
Employee employeeMock = PowerMockito.mock(Employee.class);
PowerMockito.when(employeeMock.isNew()).thenReturn(true);
PowerMockito.mockStatic(EmployeeIdGenerator.class);
WelcomeEmail welcomeEmailMock = PowerMockito.mock(WelcomeEmail.class);
PowerMockito.whenNew(WelcomeEmail.class).withArguments(employeeMock, "Welcome").thenReturn(welcomeEmailMock);
EmployeeService employeeService = new EmployeeService();
employeeService.saveEmployee(employeeMock);

PowerMockito.verifyNew(WelcomeEmail.class).withArguments(employeeMock, "Welcome");
Mockito.verify(welcomeEmailMock).send();
}
}
}

从上面示例中可以看到,通过PowerMock的链式语法PowerMockito.whenNew().withArguments().thenReturn()
可以mock类的构造函数,通过PowerMockito.verifyNew().withArguments()验证类的构造函数。

Answer模式

在某些边缘的情况下不可能通过简单地通过PowerMockito.when().thenReturn()对方法进行模拟,这时可以使用Answer接口。

@Test
public void shouldReturnCountOfEmployeesFromTheServiceWithDefaultAnswer() {
Employee employeeMock = PowerMockito.mock(Employee.class);
PowerMockito.when(employeeMock.isNew()).thenReturn(true);
EmployeeService mock = PowerMockito.mock(EmployeeService.class, new Answer() {
public Object answer(InvocationOnMock invocation) {
return 10;
}
});
}
}

上面的示例中,被mock的EmployeeService类调用任何方法都会返回10,因为都会响应Answer接口的answewr方法。此时我们就需要InbocationOnMock类提供的方法进行响应分流,它支持的方法如下:

  • callRealMethod():调用真正的方法 。
  • getArguments():获取所有参数。
  • getMethod():返回mock实例调用的方法。
  • getMock():获取mock实例。

使用spy进行部分模拟

示例中的待测试类EmployeeService

public class EmployeeService{
public void saveEmployee(Employee employee) {
if(employee.isNew()) {
createEmployee(employee);
return;
}
employee.update();
}

void createEmployee(Employee employee) {
employee.setEmployeeId(EmployeeIdGenerator.getNextId());
employee.create();
WelcomeEmail emailSender = new WelcomeEmail(employee,
"Welcome");
emailSender.send();
}
}

测试类EmployeeServiceTest

public class EmployeeServiceTest extends PowerMockTestCase {
@Test
public void shouldInvokeTheCreateEmployeeMethodWhileSavingANewEmployee() {

final EmployeeService spy = PowerMockito.spy(new EmployeeService());
final Employee employeeMock = PowerMockito.mock(Employee.class);
PowerMockito.when(employeeMock.isNew()).thenReturn(true);
PowerMockito.doNothing().when(spy).createEmployee(employeeMock);
spy.saveEmployee(employeeMock);
Mockito.verify(spy).createEmployee(employeeMock);
}
}

PowerMockito.spy()的类,它的方法只能使用PowerMockito.doNothing()PowerMockito.doReturn()PowerMockito.doThrow()来模拟,否则调用方法时都是真实调用。也就是说被mock的类,里面的方法全都是被mock的,如果想真实调用某个方法,需要用callRealMethod方法。被spy的类,里面的方法是没有被mock的,调用时候是真实调用,除非单独mock里面的某个方法。

模拟私有方法

示例中待测试的类为EmployeeService

 public class EmployeeService{
private void createEmployee(Employee employee) {
employee.setEmployeeId(EmployeeIdGenerator.getNextId());
employee.create();
WelcomeEmail emailSender = new WelcomeEmail(employee, "Welcome");
emailSender.send();
}
}

测试类为EmployeeServiceTest

public class   EmployeeServiceTest{
@PrepareForTest({EmployeeIdGenerator.class, EmployeeService.class})
public class EmployeeServiceTest extends PowerMockTestCase {
@Test
public void shouldInvokeTheCreateEmployeeMethodWhileSavingANewEmployee() throws Exception {
final EmployeeService spy = PowerMockito.spy(new EmployeeService());
final Employee employeeMock = PowerMockito.mock(Employee.class);
PowerMockito.when(employeeMock.isNew()).thenReturn(true);
PowerMockito.doNothing().when(spy, "createEmployee", employeeMock);
spy.saveEmployee(employeeMock);
PowerMockito.verifyPrivate(spy).invoke("createEmployee", employeeMock);
}
}
}

从示例中看到,测试方法体中同样没有什么特殊的存在,只是在EmployeeServiceTest类上使用了两个注解来申明将要用到PowerMock的能力。

TestNG和PowerMock的Maven配置

在Maven的pom文件中需要配置如下信息:

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.7.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-support</artifactId>
<version>1.7.0</version>
</dependency>

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-testng</artifactId>
<version>1.7.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-testng-common</artifactId>
<version>1.7.0</version>
</dependency>

<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.10</version>
<scope>test</scope>
</dependency>

单元测试规范

我们后端Java开发需要使用上述的TestNG和PowerMock两个工具进行单元测试编写,以下是我们应该遵循的一些规范:

  • Team Leader在将PRD拆分为开发任务时,对任务的估时要包含单元测试的编写时间。
  • Team Leader在Review时,单元测试是否编写和功能性是否开发完成同等重要,即一个任务的交付不只是功能按需开发完毕,同时要包含完整的单元测试。
  • 我们现在开发实体时都是基于标准自定义实体的流程进行开发,即每个实体都会有一个XXXBusinessService,那么对应的测试类应该是XXXBusinessServiceTest。
  • 在SaaS层做单元测试,只对业务逻辑进行验证,所有用到的Paas层的服务及SaaS层的其他服务都需要进行mock,即我们这层的单元测试不会真正的对DAO进行操作。
  • 测试中需要用的mock类和参数应该在前置方法中统一处理,测试方法中按需使用即可,如有特别需要的,可以在测试方法中单独处理。
  • 每个XXXBusinessService中的方法应该对应多个测试方法,即方法体里的每个分支对应一个测试方法,这里的分支指if elsetry catchswitch case
  • 每个待测方法至少应该有对应的三个测试方法:
    • 对入参健壮性校验的测试方法。
    • 正向主业务逻辑的测试方法。
    • 反向主业务逻辑的测试方法。
  • 测试方法中要进行方法是否调用的验证、方法调用次数的验证、返回结果的预期验证。
  • 如果被测试的public方法中用了private方法,那么需要对private方法进行mock。然后单独对该private方法写单元测试进行验证。
  • 单元测试写完后,之后只要对方法进行修改,在提交前必须要全局跑一遍单元测试,保证没有问题后再提交代码。
  • 测试方法的命名使用驼峰形式,并且要尽可能表达测试目的,比如public void shouldInvokeTheCreateEmployeeMethodWhileSavingANewEmployee()
  • 测试方法除了命名以外,重要逻辑需要有注释进行补充说明。

单元测试示例

下面以派工的一个方法为例,待测试类为FieldJobBusinessServiceImpl,待测试的方法为querySupportStaff,测试类为FieldJobBusinessServiceImplTest

// 待测方法
@Override
public ProcessorResult querySupportStaff(String operation, Long referEntityId, BusinessWebContext businessWebContext) {
ProcessorResult processorResult = new ProcessorResult();

if(StringUtils.isBlank(operation)){
processorResult.setStatusCode(CodeMessage.PARAM_NULL_ERR.getCode());
processorResult.setMessage(CodeMessage.PARAM_NULL_ERR.getMsg());
return processorResult;
}

if(!operation.equals(create) && !operation.equals(assign) && !operation.equals(transfer)){
processorResult.setStatusCode(CodeMessage.PARAM_FORMAT_ERR.getCode());
processorResult.setMessage(CodeMessage.PARAM_FORMAT_ERR.getMsg());
return processorResult;
}

if(operation.equals(create)){
processorResult = queryStaff4Create(referEntityId, businessWebContext);
}else if(operation.equals(assign) || operation.equals(transfer)){
processorResult = queryStaff4AssignOrTransfer(businessWebContext);
}

return processorResult;
}

上面的待测方法一共有四个分支,方法实现时用到了两个私有方法queryStaff4CreatequeryStaff4AssignOrTransfer。测试类中对该方法的测试方法有五个,分别为:

  • 参数为空时的测试:
/**
* 测试参数为空时的逻辑
* @throws Exception
*/

@Test
public void querySupportStaffWithNullParameters() throws Exception {
String operation = null;
Long referEntityId = null;
ProcessorResult processorResult = fieldJobBusinessServiceImpl.querySupportStaff(operation, referEntityId, businessWebContext);
Assert.assertTrue(processorResult.getStatusCode() == CodeMessage.PARAM_NULL_ERR.getCode());
Assert.assertTrue(processorResult.getMessage().equals(CodeMessage.PARAM_NULL_ERR.getMsg()));
}
  • 参数格式不正确时的测试:
/**
* 测试参数格式不正确时的逻辑
* @throws Exception
*/

@Test
public void querySupportStaffWithWrongParameters() throws Exception {
String operation = "wrong operation";
Long referEntityId = null;
ProcessorResult processorResult = fieldJobBusinessServiceImpl.querySupportStaff(operation, referEntityId, businessWebContext);
Assert.assertTrue(processorResult.getStatusCode() == CodeMessage.PARAM_FORMAT_ERR.getCode());
Assert.assertTrue(processorResult.getMessage().equals(CodeMessage.PARAM_FORMAT_ERR.getMsg()));
}
  • 创建派工单时,获取所有人场景的测试:
/**
* 测试操作参数为create时的逻辑,即创建派工单时,获取所有人调用
* @throws Exception
*/

@Test
public void querySupportStaffWithCreateOperation() throws Exception {
String operation = "create";
Long referEntityId = 1L;

ProcessorResult mockSuccessProcessorResult = new ProcessorResult();
mockSuccessProcessorResult.setStatusCode(CodeMessage.SUCCESS.getCode());
mockSuccessProcessorResult.setMessage(CodeMessage.SUCCESS.getMsg());

FieldJobBusinessServiceImpl spyFieldJobBusinessServiceImpl = PowerMockito.spy(new FieldJobBusinessServiceImpl());
// 在public方法中mock掉private方法,后面会对private方法写单独的单元测试,这里当调用queryStaff4Create方法时什么也不做,并直接返回上面构造好的成功的ProcessorResult
PowerMockito.doReturn(mockSuccessProcessorResult).when(spyFieldJobBusinessServiceImpl,"queryStaff4Create", Mockito.any(Long.class), Mockito.any(BusinessWebContext.class));

ProcessorResult processorResult = spyFieldJobBusinessServiceImpl.querySupportStaff(operation, referEntityId, businessWebContext);
// 验证queryStaff4Create方法被调用,并且只被调用过一次
PowerMockito.verifyPrivate(spyFieldJobBusinessServiceImpl, Mockito.times(1)).invoke("queryStaff4Create", referEntityId, businessWebContext);
// 验证queryStaff4AssignOrTransfer方法没有被调用过
PowerMockito.verifyPrivate(spyFieldJobBusinessServiceImpl, Mockito.never()).invoke("queryStaff4AssignOrTransfer", businessWebContext);
Assert.assertTrue(processorResult.getStatusCode() == CodeMessage.SUCCESS.getCode());
}
  • 分配派工单时,获取所有人场景的测试:
/**
* 测试操作参数为assign时的逻辑,即分配派工单时,获取所有人调用
* @throws Exception
*/

@Test
public void querySupportStaffWithAssignOperation() throws Exception {
String operation = "assign";
Long referEntityId = 1L;

ProcessorResult mockSuccessProcessorResult = new ProcessorResult();
mockSuccessProcessorResult.setStatusCode(CodeMessage.SUCCESS.getCode());
mockSuccessProcessorResult.setMessage(CodeMessage.SUCCESS.getMsg());

FieldJobBusinessServiceImpl spyFieldJobBusinessServiceImpl = PowerMockito.spy(new FieldJobBusinessServiceImpl());
// 在public方法中mock掉private方法,后面会对private方法写单独的单元测试,这里当调用queryStaff4AssignOrTransfer方法时什么也不做,并直接返回上面构造好的成功的ProcessorResult
PowerMockito.doReturn(mockSuccessProcessorResult).when(spyFieldJobBusinessServiceImpl,"queryStaff4AssignOrTransfer", Mockito.any(BusinessWebContext.class));

ProcessorResult processorResult = spyFieldJobBusinessServiceImpl.querySupportStaff(operation, referEntityId, businessWebContext);
// 验证queryStaff4AssignOrTransfer方法被调用,并且只被调用过一次
PowerMockito.verifyPrivate(spyFieldJobBusinessServiceImpl, Mockito.times(1)).invoke("queryStaff4AssignOrTransfer", businessWebContext);
// 验证queryStaff4Create方法没有被调用过
PowerMockito.verifyPrivate(spyFieldJobBusinessServiceImpl, Mockito.never()).invoke("queryStaff4Create", referEntityId, businessWebContext);
Assert.assertTrue(processorResult.getStatusCode() == CodeMessage.SUCCESS.getCode());
}
  • 转移派工单时,获取所有人场景的测试:
/**
* 测试操作参数为transfer时的逻辑,即转移派工单时,获取所有人调用
* @throws Exception
*/

@Test
public void querySupportStaffWithTransferOperation() throws Exception {
String operation = "transfer";
Long referEntityId = 1L;

ProcessorResult mockSuccessProcessorResult = new ProcessorResult();
mockSuccessProcessorResult.setStatusCode(CodeMessage.SUCCESS.getCode());
mockSuccessProcessorResult.setMessage(CodeMessage.SUCCESS.getMsg());

FieldJobBusinessServiceImpl spyFieldJobBusinessServiceImpl = PowerMockito.spy(new FieldJobBusinessServiceImpl());
// 在public方法中mock掉private方法,后面会对private方法写单独的单元测试,这里当调用queryStaff4AssignOrTransfer方法时什么也不做,并直接返回上面构造好的成功的ProcessorResult
PowerMockito.doReturn(mockSuccessProcessorResult).when(spyFieldJobBusinessServiceImpl,"queryStaff4AssignOrTransfer", Mockito.any(BusinessWebContext.class));

ProcessorResult processorResult = spyFieldJobBusinessServiceImpl.querySupportStaff(operation, referEntityId, businessWebContext);
// 验证queryStaff4AssignOrTransfer方法被调用,并且只被调用过一次
PowerMockito.verifyPrivate(spyFieldJobBusinessServiceImpl, Mockito.times(1)).invoke("queryStaff4AssignOrTransfer", businessWebContext);
// 验证queryStaff4Create方法没有被调用过
PowerMockito.verifyPrivate(spyFieldJobBusinessServiceImpl, Mockito.never()).invoke("queryStaff4Create", referEntityId, businessWebContext);
Assert.assertTrue(processorResult.getStatusCode() == CodeMessage.SUCCESS.getCode());
}

从上面的测试方法中可以看到,对用到的私有方法进行了mock,并且对方法的调用进行了验证。代码中都有注释,这里不再累赘。测试方法写完后,将其配置在TestNG的配置文件中/scr/test/resources/testng.xml

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="Field Service Cloud" verbose="1">
<test name="FieldJobBusinessService">
<classes>
<class name="com.rkhd.business.fsc.fieldjob.FieldJobBusinessServiceTest">
<methods>
<include name="querySupportStaffWithNullParameters"/>
<include name="querySupportStaffWithWrongParameters"/>
<include name="querySupportStaffWithCreateOperation"/>
<include name="querySupportStaffWithAssignOperation"/>
<include name="querySupportStaffWithTransferOperation"/>
</methods>
</class>
</classes>
</test>
</suite>

以后有可能整个manager-service会共用一个配置文件,所以使用<suite>作为产品线的区分,使用<test>作为实体的区分,鼠标右键点击该配置文件,即可看到执行测试的选项。

针对querySupportStaff方法,以上这个五个测试方法可以完全覆盖该方法承载的业务逻辑和场景,并且达到了该方法内百分百的代码覆盖率。使用IDEA的Coverage插件,可以计算测试方法对代码的覆盖率,并可以生成HTML文档。

虽然querySupportStaff测试完了,但是其中的两个私有方法是被mock掉的,并且是都是待测类中的方法,所以接下来就需要对这两个私有方法单独再写单元测试进行验证,写单元测试的规则与规范和querySupportStaff方法的单元测试一致。

总结

希望该文档能作为后端Java开发保证代码质量的指导手册,养成良好的单元测试编写习惯,最终让我们具备认为完成功能开发只是完成了一半任务的优秀素养。

分享到: