在实际进行单元测试的过程中,我们会发现被测代码通常会调用一些外部依赖或者尚未实现的方法,导致编写单元测试代码相当困难。针对这种情况,我们就需要对这些依赖的对象进行伪造注入,使得被测代码能够顺利运行,并能够对运行结果进行验证。
Java开发中常用的Mock框架包括PowerMock, JMockit, Mockito, EasyMock, JMock等等,其中PowerMock是在Mockito和EasyMock的基础上封装实现的,API简单易用,功能相比其他框架也更强大。这里主要介绍如何使用PowerMock和Mockito框架来应对单元测试中常见的Mock场景。
基础篇中有提到,在一个单元测试用例中通常包括三个阶段,首先构造测试条件,对存在的依赖项创建Stub测试桩,设置好when.then,或者创建Mock模拟对象,用于最后的结果验证,然后第二步对被测代码执行真正的调用,最后一步在用例中添加断言,验证代码运行结果是否与期望的一致。
1. 依赖包
我们首先将测试需要的依赖包引入。在使用Maven进行构建的项目中引入的依赖包如下:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.3</version>
<scope>test</scope>
</dependency>
2. Mock阶段
PowerMock作为非受限的Mock框架,针对各种场景都提供了方法。
2.1 普通Mock
被测代码如下:
public class Book {
public int getResult() {
throw new UnsupportedOperationException();
}
}
测试用例如下:
public class BookTest {
@Test
public void test() {
Book stub = PowerMockito.mock(Book.class);
PowerMockito.when(stub.getResult()).thenReturn(5);
......
}
}
当Mock的方法中没有返回值或者需要抛出异常时,方法如下:
PowerMockito.doNothing().when(stub).getResult();
PowerMockito.doThrow(new Exception()).when(stub).thenReturn();
2.2 模拟静态方法或者final类/方法
被测代码如下:
public final class Book {
public static int getResult() {
throw new UnsupportedOperationException();
}
}
测试用例如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() {
PowerMockito.mockStatic(Book.class);
PowerMockito.when(Book.getResult()).thenReturn(5);
......
}
}
不是普通Mock的情况都需要同时添加RunWith和PrepareForTest注解,这里的PrepareForTest注解也可以移到类的头上,当需要Mock多个静态类时,PrepareForTest的参数需要用大括号括起来。
2.3 模拟私有方法
被测代码如下:
public class Book {
private int getResult() {
throw new UnsupportedOperationException();
}
}
测试用例如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() throws Exception {
Book stub = PowerMockito.mock(Book.class);
PowerMockito.when(stub, "getResult").thenReturn(5);
......
}
}
2.4 模拟构造方法
被测代码如下:
public class Book {
public Book() {
throw new UnsupportedOperationException();
}
}
测试用例如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() throws Exception {
Book stub = PowerMockito.mock(Book.class);
PowerMockito.whenNew(Book.class).withNoArguments().thenReturn(stub);
......
}
}
如果这里的构造函数需要传入参数,那么whenNew之后可以调用withArguments()方法。
2.5 使用spy进行部分模拟
当一个类只有少量的方法未实现,或者我们希望只对少量的几个方法进行Mock,其他方法依然调用原来的逻辑时,我们就可以使用spy来进行部分模拟。
被测代码如下:
public class Book {
public int getPrice() {
throw new UnsupportedOperationException();
}
public int getResult() {
return this.getPrice() + 2;
}
}
测试用例如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() throws Exception {
Book spy = PowerMockito.spy(new Book());
PowerMockito.doReturn(5).when(spy).getPrice();
int result = spy.getResult();
Assert.assertEquals(7, result);
}
}
这里有两个注意点,一是如果这里使用PowerMockito.when(spy.getPrice()).thenReturn(5);
进行模拟,getPrice()方法依然会被调用;二是在调用被测代码时需要使用spy对象进行,如果使用原来的实例,则这里的Mock操作就不能生效。
2.6 访问对象的属性
我们可以通过Whitebox的setInternalState和getInternalState设置和访问对象的私有属性。例如假设Book对象有一个price私有属性,我们可以通过Whitebox.getInternalState(book, "num")
访问该属性。
2.7 禁用非预期行为
当我们的被测方法中调用了某些尚未实现或者不希望被调用的方法时,我们在测试时可以暂时将其禁用。
被测代码如下:
public class Book {
public void doOtherThings() {
throw new UnsupportedOperationException();
}
public int getResult() {
this.doOtherThings();
return 2;
}
}
测试用例如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() throws Exception {
Book book = new Book();
PowerMockito.suppress(PowerMockito.method(Book.class, "doOtherThings"));
Assert.assertEquals(2, book.getResult());
}
}
2.8 参数匹配
被测代码如下:
public class Book {
public int getResult(int count) {
throw new UnsupportedOperationException();
}
}
测试用例如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() throws Exception {
Book stub = PowerMockito.mock(Book.class);
PowerMockito.when(stub.getResult(Mockito.anyInt())).thenReturn(5);
}
}
我们直接使用Mockito的api接口,这里还支持Mockito的如下参数匹配规则:eq, matches, any(anyBoolean, anyByte, anyShort, anyChar, anyInt, anyLong, anyFloat, anyDouble, anyList, anyCollection, anyMap, anySet等), isNull, isNotNull, endsWith, isA等。
另外,当调用的方法中包含多个参数时,如果使用了参数匹配器,那么每个参数都需要使用,否则都不使用。
2.9 doAnswer支持复杂逻辑
当对方法进行Mock时,如果响应中希望包含比较复杂的逻辑,例如我们需要根据方法参数计算返回值,这种情况我们可以使用doAnswer来实现。
被测代码如下:
public class Book {
public int getResult(int count) {
throw new UnsupportedOperationException();
}
}
测试用例如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() throws Exception {
Book stub = PowerMockito.mock(Book.class);
PowerMockito.when(stub.getResult(Mockito.anyInt())).thenAnswer(invocation -> {
int count = (int) invocation.getArguments()[0];
return 3 * count;
});
......
}
}
2.10 使用注解进行模拟
当被测对象包含多个需要被Mock的属性时,我们可以通过注解的方式非常简单地 进行Mock注入。
被测代码如下:
public class Book {
@Autowired
private ComputeDao computeDao;
public int getResult(int count) {
throw new UnsupportedOperationException();
}
}
测试代码如下:
@RunWith(PowerMockRunner.class)
public class BookTest {
@InjectMocks
private Book book;
@Mock
private ComputeDao computeDao;
@Test
public void test() throws Exception {
......
}
}
这里会创建一个Book 实例,这个实例是真实对象,以及一个Mock的ComputeDao对象,然后将这个Mock的computeDao自动注入到了book实例中。
2.11 按照调用顺序进行模拟
当一个方法被调用多次,我们需要为每次调用设置不同的返回值时,我们可以使用如下方法。
@RunWith(PowerMockRunner.class)
public class BookTest {
@Test
@PrepareForTest(Book.class)
public void test() throws Exception {
Book stub = PowerMockito.mock(Book.class);
PowerMockito.when(stub.getResult(Mockito.anyInt())).thenReturn(3).thenReturn(5);
Assert.assertEquals(3, stub.getResult(0));
Assert.assertEquals(5, stub.getResult(0));
}
}
这里设置了两个返回值,前两次调用会分别返回3和5,之后再调用时则始终按照最后一个返回值进行返回。
3. Call阶段
在调用被测方法时,通常我们是直接通过obj.invokeMethod(…)的方式调用即可,但是当我们的被测方法是私有方法时,通过前面的方式就调用不了了,这时我们可以使用PowerMock的Whitebox实现私有方法调用,如下:
Whitebox.invokeMethod(instance, "privateMethod", arg0, ...);
Book book = Whitebox.invokeConstructor(Book.class, arg0, ...);
当类的构造函数也为私有的时候,我们也可以通过这种方法创建对象。
4. Verify阶段
在每个测试用例的最后阶段,我们需要验证程序的运行结果是否与我们期望的一致。这里通常使用Mockito提供的方法进行验证,针对静态方法的验证,PowerMock也提供了相应的验证方法。
4.1 数值验证
对于数值验证,等待运行结果出来后,直接通过org.junit.Assert.assertxxx这一系列方法即可进行验证。例如:
Assert.assertEquals(3, book.getPrice());
4.2 方法调用验证
我们还可以对某个具体方法是否被调用了和未被调用进行验证,代码示例如下:
Mockito.verify(mock).getResult(Mockito.anyInt());
Mockito.verify(mock, Mockito.never()).getResult(Mockito.anyInt());
为了调用方便,我们可以将Mockito的这一系列方法统一引入,import static org.mockito.Mockito.*;
。
这里verify方法的第二个参数支持传入各种验证条件,例如:
- time(n): 验证方法是否调用了n次
- atLeastOnce(): 至少调用了一次
- atLeast(n): 至少调用了n次
- atMost(n): 至多调用了n次
- never(): 从未被调用过
如果不传入参数,则验证方法是否刚好被调用了一次。
4.3 方法调用顺序验证
需要验证多个方法的调用顺序的时候,使用InOrder。
InOrder inOrder = Mockito.inOrder(mock);
inOrder.verify(mock).invokeMethod1();
inOrder.verify(mock).invokeMethod2();
这里验证invokeMethod1和invokeMethod2方法是否按照先后顺序进行调用。
4.4 静态方法调用验证
对静态方法调用的验证如下:
PowerMockito.verifyStatic(times(2));
Book.getDefault();
这里验证Book.getDefault()方法是否被调用了2次。
4.5 参数捕捉
有时候我们不光是需要验证某个方法是否被调用了,还需要对传入的参数进行验证。
ArgumentCaptor<Integer> argCaptor = ArgumentCaptor.forClass(Integer.class);
PowerMockito.verifyStatic();
Book.getDefault(argCaptor.capture());
Assert.assertEquals(4, (int) argCaptor.getValue());