本文介绍的是Java语言的单元测试框架,分别介绍Junit、Mockito、Powermock三种工具的特点,并附上了用于演示的Demo案例。
什么是单元测试
单元测试是指,对软件中的最⼩可测试单元在与程序其他部分相隔离的情况下进⾏
检查和验证的⼯作,这⾥的最⼩可测试单元通常是指函数或者类。
单元测试的好处
单元测试通常由开发⼯程师完成,⼀般会伴随开发代码⼀起递交⾄代码库。单元测试属于最严格
的软件测试⼿段,是最接近代码底层实现的验证⼿段,可以在软件开发的早期以最⼩的成本保证
局部代码的质量。
如何做好单元测试
需要测试哪些东西:
- 结果是否正确
- 边界条件
- 空值或者不完整的值
- 格式错误的数据
- 完全伪造或者不一致的输入数据
- 意料之外的值
- 检查反向关联
- 检查异常:强制检查异常情况
- 性能特性
什么是好的单元测试:
单元测试的三个步骤
在Spring中使用Junit进行单元测试
阿里代码规约手册中几条关于单元测试的强制规范
- 不允许使用
syetem.out
进行人肉验证,必须使用assert
进行验证
- 保持单元测试的独立性,每一个测试案例互不影响
- 核心业务,核心代码,核心模块的新增代码必须保证单元测试通过
Junit介绍和入门
Junit
是一套框架(用于JAVA
语言),由 Erich Gamma
和 Kent Beck
编写的一个回归测试框架(regression testing framework)
,即用于白盒测试。现阶段的最新版本号是4.12
,JUnit5
目前正在测试中,所以这里还是以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) ) );
assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) );
assertThat( testedString, containsString( "developerWorks" ) );
assertThat( testedNumber, greaterThan(16.0) );
assertThat( iterableObject, hasItem ( "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;
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;
public class AccountantTest {
@Mock Calculator calculator;
@InjectMocks Accountant accountant;
@Before public void setUp() throws Exception { 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
注解有两个可选的参数,分别为timeout
和expectd
。除@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;
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
层做一些初始化的工作从而保证测试通过,就可以将Dao
层Mock
出来。Mock
出来的对象不是真实的对象,而是具备和真实对象相同行为的对象。比如List mockList = mock(List.class)
,这个mockList
并不是真实的List
,但同样又add()
,clear()
,size()
等方法。
Mockito介绍和入门
Mockito
是 Mock
数据的测试框架,简化了对有外部依赖的类的单元测试。
常用的几个注解:
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;
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;
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;
public class AccountantTest {
@Mock Calculator calculator;
@InjectMocks Accountant accountant;
@Before public void setUp() throws Exception { 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.*;
@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"); 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();
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;
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.*;
@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(); }
|
引用