单元测试:从Junit到Powermock

本文介绍的是Java语言的单元测试框架,分别介绍Junit、Mockito、Powermock三种工具的特点,并附上了用于演示的Demo案例。

什么是单元测试

单元测试是指,对软件中的最⼩可测试单元在与程序其他部分相隔离的情况下进⾏
检查和验证的⼯作,这⾥的最⼩可测试单元通常是指函数或者类。

单元测试的好处

单元测试通常由开发⼯程师完成,⼀般会伴随开发代码⼀起递交⾄代码库。单元测试属于最严格
的软件测试⼿段,是最接近代码底层实现的验证⼿段,可以在软件开发的早期以最⼩的成本保证
局部代码的质量。

如何做好单元测试

需要测试哪些东西:

  • 结果是否正确
  • 边界条件
    • 空值或者不完整的值
    • 格式错误的数据
    • 完全伪造或者不一致的输入数据
    • 意料之外的值
  • 检查反向关联
    • 为了检查数据是否插入成功,检查能否查询出来
  • 检查异常:强制检查异常情况
  • 性能特性

什么是好的单元测试:

  • 自动化
  • 独立性
  • 可重复

单元测试的三个步骤

  • 准备数据、行为
  • 测试目标模块
  • 验证测试结果

在Spring中使用Junit进行单元测试

阿里代码规约手册中几条关于单元测试的强制规范

  • 不允许使用syetem.out进行人肉验证,必须使用assert进行验证
  • 保持单元测试的独立性,每一个测试案例互不影响
  • 核心业务,核心代码,核心模块的新增代码必须保证单元测试通过

Junit介绍和入门

Junit是一套框架(用于JAVA语言),由 Erich GammaKent Beck 编写的一个回归测试框架(regression testing framework),即用于白盒测试。现阶段的最新版本号是4.12JUnit5目前正在测试中,所以这里还是以JUnit4为准。

使用assertThat语法

JUnit4.4引入了Hamcrest框架(匹配器框架),Hamcest提供了一套匹配符Matcher,这些匹配符更接近自然语言,可读性高,更加灵活。使用全新的断言语法:assertThat,结合Hamcest提供的匹配符,只用这一个方法,就可以实现所有的测试。

assertThat语法如下:

1
2
assertThat(T actual, Matcher<T> matcher);
assertThat(String reason, T actual, Matcher<T> matcher);

其中actual为需要测试的变量,matcher为使用Hamcrest的匹配符来表达变量actual期望值的声明。

1
2
3
4
5
6
7
8
9
10
assertThat( testedNumber, allOf( greaterThan(8), lessThan(16) ) );
//allOf匹配符表明如果接下来的所有条件必须都成立测试才通过,相当于“与”(&&)2、
assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) );
//anyOf匹配符表明如果接下来的所有条件只要有一个成立则测试通过,相当于“或”(||)
assertThat( testedString, containsString( "developerWorks" ) );
//containsString匹配符表明如果测试的字符串testedString包含子字符串"developerWorks"则测试通过
assertThat( testedNumber, greaterThan(16.0) );
//greaterThan匹配符表明如果所测试的数值testedNumber大于16.0则测试通过
assertThat( iterableObject, hasItem ( "element" ) );
//hasItem匹配符表明如果测试的迭代对象iterableObject含有元素“element”项则测试通过

常见注解介绍

引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>1.3</version>
<scope>test</scope>
</dependency>

新建一个测试用的类:Calculator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.lazyallen.blog;

import java.util.Objects;

/**
* @author allen
* @Date 2019-06-09
*/
public class Calculator {


public Double addition(Double x,Double y){
if (Objects.isNull(x) || Objects.isNull(y)) {
throw new NullPointerException("参数不能为空");
}
return x+y;
}

public Double division(Double x, int y){
if (0==y){
throw new IllegalArgumentException("除数y不能为0");
}
return x/y;
}

public Double multiplication(Double x, Double y){
if (Objects.isNull(x) || Objects.isNull(y)) {
throw new NullPointerException("参数不能为空");
}
return x*y;
}

public void version(){
System.out.printf("v1.0");
}
}

生成对应的测试类:CalculatorTest

小技巧:在IDEA中,使用ctrl + shift + T 的快捷键可以快速生成测试类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.lazyallen.blog;

import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.is;



/**
* @author allen
* @Date 2019-06-09
*/
public class AccountantTest {

@Mock
Calculator calculator;

@InjectMocks
Accountant accountant;

@Before
public void setUp() throws Exception {
// 初始化测试用例类中由Mockito的注解标注的所有模拟对象
MockitoAnnotations.initMocks(this);
}

@Test
public void testCalculateSalary() {
when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0);
when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0);
Double salary = accountant.calculateSalary(10.0,10.0);
assertThat(salary,is(16.0));
}

}

@Test注解有两个可选的参数,分别为timeoutexpectd。除@Test注解之外,还有如下注解:

  • @Before 注解的作用是使被标记的方法在测试类里每个方法执行前调用;同理 After 使被标记方法在当前测试类里每个方法执行后调用。
  • @BeforeClass 注解的作用是使被标记的方法在当前测试类被实例化前调用;同理 @AfterClass 使被标记的方法在测试类被实例化后调用。
  • @Ignore 注解的作用是使被标记方法暂时不执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.lazyallen.blog;

import org.junit.*;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

/**
* @author allen
* @Date 2019-06-09
*/
public class Calculatortest2 {

public Calculatortest2() {
System.out.println("Constructor");
}

@BeforeClass
public static void beforeThis() throws Exception {
System.out.println("BeforeClass");
}

@AfterClass
public static void afterThis() throws Exception {
System.out.println("AfterClass");
}

@Before
public void setUp() throws Exception {
System.out.println("Before");
}

@After
public void tearDown() throws Exception {
System.out.println("After");
}

@Test
public void evaluate() throws Exception {
Calculator calculator = new Calculator();
int sum = calculator.addition(1,1);
assertThat(sum, is(2));
System.out.println("Test evaluate");
}

@Test
public void idiot() throws Exception {
System.out.println("Test idiot");
}

@Ignore
public void ignoreMe() throws Exception {
System.out.println("Ignore");
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
BeforeClass
Constructor
Before
Test idiot
After
Constructor
Before
Test evaluate
After
AfterClass

Process finished with exit code 0

从控制台输出可以得到以下两点信息:

  • 测试类在测试每一个case时,会重新实例化一次测试类,为的是保证每个case是独立隔离的。
  • BeforeClass是在测试类初始化之前执行,Before是在每一个case运行前执行,我们可以用该注解在测试类初始化的时候准备一些测试的数据。

使用Mockito模拟来Mock对象

为什么需要Mock

在做单元测试的时候,经常出现这样的情况,在需要测试中模块中包含其他依赖的模块,有时候去对这个依赖的模块做单元测试比较高,或者无法进行单元测试。例如,在Java Web项目中通常是分层的,我们需要对Service层进行单元测试,由于Service需要依赖Dao层,我们又不希望对Service的同时又为Dao层做一些初始化的工作从而保证测试通过,就可以将DaoMock出来。Mock出来的对象不是真实的对象,而是具备和真实对象相同行为的对象。比如List mockList = mock(List.class),这个mockList并不是真实的List,但同样又add(),clear(),size()等方法。

Mockito介绍和入门

MockitoMock 数据的测试框架,简化了对有外部依赖的类的单元测试。

常用的几个注解:

1
2
3
4
5
6
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
<scope>test</scope>
</dependency>

这里我们写一个会计师类:Accountant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.lazyallen.blog;

/**
* @author allen
* @Date 2019-06-09
*/
public class Accountant {
private static final Double tax = 0.2;
Calculator calculator;

public Double calculateSalary(Double a, Double b){
Double entireSalary = calculator.addition(a,b);
Double deservedSalary = calculator.multiplication(entireSalary,(1-tax));
return deservedSalary;
}

public Double calculateOddMonthSalary(Double a, Double b){
int month = DateUtils.getCurrentMonth();
if(month%2!=0){
Double entireSalary = calculator.addition(a,b);
Double deservedSalary = calculator.multiplication(entireSalary,(1-tax));
return deservedSalary;
}
return 0.0;
}

private String sayHello(){
System.out.println("hello");
return "hello";
}

public void printSayHello(){
String hello = this.sayHello();
System.out.println(hello);
}

public Accountant(Calculator calculator) {
this.calculator = calculator;
}

public Accountant() {
}
}

对应的测试类:

小技巧:静态导入 org.mockito.Mockito.*;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.lazyallen.blog;

import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
* @author allen
* @Date 2019-06-09
*/
public class AccountantTest2 {


Calculator calculator;
Accountant accountant;

@Before
public void setUp() throws Exception {
calculator = mock(Calculator.class);
accountant = new Accountant(calculator);

}

@Test
public void testCalculateSalaryUseMockMethod() {
when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0);
when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(80.0);
Double salary = accountant.calculateSalary(10.0,10.0);
assertThat(salary,is(80.0));
}
}

Mockito 支持通过静态方法mock()Mock 对象,或者通过 @Mock 注解,来创建 Mock 对象。

假如说,你需要使用@Mock注解,则需要初始化测试用例类中由Mockito的注解标注的所有模拟对象,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.lazyallen.blog;

import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.is;



/**
* @author allen
* @Date 2019-06-09
*/
public class AccountantTest {

@Mock
Calculator calculator;

@InjectMocks
Accountant accountant;

@Before
public void setUp() throws Exception {
// 初始化测试用例类中由Mockito的注解标注的所有模拟对象
MockitoAnnotations.initMocks(this);
}

@Test
public void testCalculateSalary() {
when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0);
when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0);
Double salary = accountant.calculateSalary(10.0,10.0);
assertThat(salary,is(16.0));
}

}

你也可以使用@RunWith(MockitoJUnitRunner.class)去代替MockitoAnnotations.initMocks(this);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.lazyallen.blog;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.*;

/**
* @author allen
* @Date 2019-06-09
*/
@RunWith(MockitoJUnitRunner.class)
public class AccountantTest3 {

@Mock
Calculator calculator;

@InjectMocks
Accountant accountant;

@Test
public void testCalculateSalary() {
when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0);
when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0);
Double salary = accountant.calculateSalary(10.0,10.0);
assertThat(salary,is(16.0));
}

@Test(expected = NullPointerException.class)
public void testCalculateSalary1(){
when(calculator.addition(anyDouble(),anyDouble())).thenReturn(100.0).thenThrow(new NullPointerException());
Double mockResult = calculator.addition(10.0,10.0);
System.out.println("mockResult"+mockResult);
calculator.addition(10.0,10.0);

}
}

通常建议使用@RunWith(MockitoJUnitRunner.class)的方式去初始化测试用例类中由Mockito的注解标注的所有模拟对象。

mock 出来的对象拥有和源对象同样的方法和属性,when()thenReturn() 方法是对源对象的配置,怎么理解,就是说在第一步 mock() 时,mock 出来的对象还不具备被 Mock 对象实例的行为特征,而 when(...).thenReturn(...) 就是根据条件去配置源对象的预期行为。

有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。典型的运用就是使用mock迭代器。

1
2
3
4
5
6
7
8
@Test
public void testMockIterator() {
Iterator i = mock(Iterator.class);
when(i.next()).thenReturn("hello","world");
//when(i.next()).thenReturn("hello").thenReturn("world");
String result = i.next() + " " + i.next();
assertThat(result,is("hello world"));
}

除了对方法调用结果是否正确的测试,有时还需要验证一些方法的行为,比如验证方法被调用的次数,验证方法的入参等,Mockito 通过 verify() 方法实现这些场景的测试需求。这被称为“行为测试”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testVerify() {
Calculator mock = mock(Calculator.class);
when(mock.addition(anyDouble(),anyDouble())).thenReturn(2.0);

mock.addition(1.0,1.0);
mock.version();
mock.version();

//比如说写Controller的单元测试,验证service方法是否调用
verify(mock, times(2)).version();
verify(mock, never()).multiplication(anyDouble(),anyDouble());
verify(mock, atLeastOnce()).version();
verify(mock, atLeast(2)).version();
verify(mock, atMost(3)).version();;
}

Mockito 支持通过 @Spy 注解或 spy() 方法包裹实际对象,除非明确指定对象,否则都会调用包裹后的对象。这种方式实现了对实际对象的部分自定义修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testDifferMockSpy() {
List mock = mock(ArrayList.class);
mock.add("one");
verify(mock).add("one");
System.out.println("mock[0]:"+mock.get(0));

List spy = spy(new ArrayList());
spy.add("one");
verify(spy).add("one");
System.out.println("spy[0]:"+spy.get(0));
when(spy.size()).thenReturn(100);
System.out.println("spy.size:"+spy.size());

}

PowerMockito解决了什么问题

Mockito 因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是Mock 工具不可以实现对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟,PowerMock 是在 EasyMock 以及 Mockito 基础上的扩展,通过定制类加载器等技术,PowerMock 实现了之前提到的所有模拟功能,使其成为大型系统上单元测试中的必备工具。

Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.0-RC.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.0-RC.3</version>
<scope>test</scope>
</dependency>

mock静态方法

通常,在一些工具类中会又很多静态方法,比如,我们新建一个工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.lazyallen.blog;

import java.time.LocalDate;
import java.time.LocalDateTime;

/**
* @author allen
* @Date 2019-06-09
*/
public class DateUtils {

public static int getCurrentMonth(){
LocalDate now = LocalDate.now();
return now.getMonthValue();
}
}

相应的测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.lazyallen.blog;

import javafx.beans.binding.When;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.Mockito.*;

/**
* @author allen
* @Date 2019-06-09
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({DateUtils.class,Accountant.class})
public class AccountantTest4 {

@Mock
Calculator calculator;

@InjectMocks
Accountant accountant;

@Before
public void init(){
PowerMockito.mockStatic(DateUtils.class);

}

@Test
public void testCalculateOddMonthSalary(){
PowerMockito.when(DateUtils.getCurrentMonth()).thenReturn(7);
when(calculator.addition(anyDouble(),anyDouble())).thenReturn(20.0);
when(calculator.multiplication(anyDouble(),anyDouble())).thenReturn(16.0);
Double salary = accountant.calculateOddMonthSalary(10.0,10.0);
assertThat(salary,is(16.0));
}
}

只需要注意两点:

  • @RunWith(PowerMockRunner.class)
  • @PrepareForTest({DateUtils.class,Accountant.class})

mock私有方法

1
2
3
4
5
6
@Test
public void testPrivate() throws Exception {
Accountant accountant1 = PowerMockito.spy(new Accountant());
PowerMockito.when(accountant1, "sayHello").thenReturn("你好");
accountant1.printSayHello();
}

引用