[Java] [Unit Test] Mocking Frame
转到Java之后我还没有系统地学习那些常用的mock框架,平时写代码都是模仿着别人的code东抄抄西抄抄,不会的再去stack overflow找找答案。最近发现很多新人都是这样写单元测试的,然后看看代码库,同一个包里用不同的框架的大有人在。我并不是说这样不好,但是这样很容易误导新人。我遇见过两次有人在写单元测试的时候,annotation用的是一个mock框架,在setup的时候又是用的另一个框架,然后纠结着怎么跑不通,各种错误呢!🤷♀️我自己摸爬滚打了一阵之后,发现写单元测试的时候最好的参考文档并不是别人的代码,而是官方的Tutorial。
于是我开了这个系列,打算讲讲大家常用的JMockit和Mockito等框架,也借此机会系统学习一遍。这里假设读者对单元测试的基本知识、AAA (Arrange, Act, Assert)的三段式以及什么是mock都了解,而且这里的mock框架与你所用的单元测试的框架Junit/TestNG没有关系。
对于每个框架,我会先介绍它的主要的annotation和功能;然后以自己经常写到的use cases举例说明用这个框架该怎么写。
Mockito
1. Mockito能mock什么?
Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.
Mockito在StackOverflow上被评为“the best mock framework for Java”肯定是有一定的道理的。它最大的缺点可能是无法mock那些static和final class或者private方法,但是PowerMockito弥补了这些不足。而且在面向对象的设计中我们越来越少地使用那些静态的或面向过程的设计,所以绝大多数情况Mockito都能很方便地用起来。
2. Annotations
- @Mock:为被修饰的类型创建一个虚拟的实现,并把这个虚拟实现的实例赋给被修饰的对象。
- @InjectMocks:被修饰的对象会自动注入被@Mock和@Spy注解的成员。
- @Spy:也是一种mock的方式,只是不会为被修饰的对象创建一个虚拟的对象,但是可以mock对象的一些行为。
- @RunWith:用来绑定一个runner来初始化测试数据,例如,当使用
@Mock
时我们需要用@RunWith(MockitoJUnitRunner.class)
修饰测试类来初始化那些mock的成员。当然也可以在setup里显示地用MockitoAnnotations.initMocks(this)
来初始化那些mock的成员。此外,在Junit中还有另一种方式,那就是利用Junit的Rule(MockitoRule)来初始化那些mock,@Rule public MockitoRule rule = MockitoJUnit.rule()
。
3. Mockito的套路
Mockito像其他的mock框架一样,提供了一套Behavior Driven Development的单元测试书写三段式:Given, When, Then。
//Given
given(calcService.add(20.0,10.0)).willReturn(30.0);
//when
double result = calcService.add(20.0,10.0);
//then
Assert.assertEquals(result,30.0,0);
所以,用Mockito写单元测试大概是下面这个流程。
0)创建mock
创建mock的方式有两种,一种是利用@Mock
标注,另一种则是直接用mock()静态方法calcService = mock(CalculatorService.class);
。
1)添加behavior
Mockito有好几种添加behavior的方式,乍一看有点儿眼花缭乱,其实主要是分为两类。
- When/Then or Given/Will
when(object.method()).thenReturn(value);
given(object.method()).willThrow(exception);
- doXX/When
之所以引入doXX是为了解决stub返回值为void的方法,因为上面的when()和given()的参数是T,当传入返回值为void的方法时会出现编译错误。
doThrow(new RuntimeException()).when(mockedList).clear();
2)执行测试
3)验证behavior
当验证behavior的时候我们一般验证指定的方法有没有被invoke,被调用了多少次,还可以capture
被invoke时的参数值,用于做进一步的assert。
verify(calcService, times(1)).add(10.0, 20.0);
verify(calcService, never()).multiply(10.0,20.0);
对于确定的次数,一般用times(x)
就可以了,比如never()
也可以写times(0)
。但是对于一些不确定但是有上下限的则可以用atLeast(int min)
,atLeastOnce()
和atMost(int max)
。
4. Use Cases
还是话不多说地从DemoService说起吧,下面我汇集了平时常用的一些情况在一个API中,然后我们尝试着用Mockito来写一下它的测试,并以此为出发点介绍Mockito的一些常见用法。
public class DemoService {
private DependencyY dependencyY;
private DependencyYY dependencyYY;
private DependencyZ dependencyZ;
public DemoService(DependencyZ dependencyZ) {
this.dependencyZ = dependencyZ;
}
public int run() {
DependencyX dependencyX = new DependencyX("inputOfX");
String x = dependencyX.getX();
List<String> y = dependencyY.getY("default");
List<String> list = new ArrayList<>();
for (String s : y) {
try {
list.add(dependencyYY.doSomethingForY(s));
} catch (DemoException ex) {
// Handle the exception
}
}
int index = doSomething(list, x);
if (StaticDependency.isEnabled() && -1 == index) {
dependencyZ.sendNotification("ErrorMessage");
}
StaticDependency.doAnything();
return index;
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({DemoService.class, StaticDependency.class})
public class DemoServiceTest {
@Mock
private DependencyX dependencyX;
@Mock
private DependencyY dependencyY;
@Mock
private DependencyYY dependencyYY;
@Mock
private DependencyZ dependencyZ;
@InjectMocks
private DemoService demoService = new DemoService(dependencyZ);
@Captor
private ArgumentCaptor<String> notificationCaptor;
@Before
public void setUp() throws Throwable {
MockitoAnnotations.initMocks(this);
PowerMockito.whenNew(DependencyX.class).withAnyArguments().thenReturn(dependencyX);
PowerMockito.mockStatic(StaticDependency.class);
PowerMockito.when(StaticDependency.isEnabled()).thenReturn(true);
}
@Test
public void testDemoService_ShouldGetCorrectIndex_WhenXExistsInY() throws Exception {
when(dependencyX.getX()).thenReturn("str2");
when(dependencyY.getY(anyString())).thenReturn(Arrays.asList("B", "A", "C"));
when(dependencyYY.doSomethingForY("B")).thenReturn("str1");
when(dependencyYY.doSomethingForY("A")).thenReturn("str2");
when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());
int result = demoService.run();
Assert.assertEquals(1, result);
PowerMockito.verifyStatic();
StaticDependency.doAnything();
verify(dependencyZ, times(0)).sendNotification(anyString());
}
@Test
public void testDemoService_ShouldSendNotification_WhenXNotExistsInY() throws Exception {
when(dependencyX.getX()).thenReturn("str3");
when(dependencyY.getY(eq("default"))).thenReturn(Arrays.asList("B", "A", "C"));
when(dependencyYY.doSomethingForY("B")).thenReturn("str1");
when(dependencyYY.doSomethingForY("A")).thenReturn("str2");
when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());
int result = demoService.run();
Assert.assertEquals(-1, result);
PowerMockito.verifyStatic();
StaticDependency.doAnything();
verify(dependencyZ, atLeastOnce()).sendNotification(notificationCaptor.capture());
Assert.assertTrue(notificationCaptor.getValue().contains("Error"));
}
}
4.1 mock被测对象中的成员有返回值的方法
如果被测对象中的成员是一个外部依赖,我们需要mock它的那些有返回值的方法,例如dependencyY.getY(x)
和dependencyYY.doSomethingForY(y)
。
这是最基本最常见的一种情况,我们直接mock这些dependency类,然后用when/then语句(given/will也类似)给我们用到的那些方法添加behavior。
when(mockedClass.method(params)).thenReturn(mockedResult);
when(dependencyY.getY(anyString())).thenReturn(Arrays.asList("B", "A", "C"));
when(dependencyYY.doSomethingForY("B")).thenReturn("str1");
when(mockedClass.method(params)).thenThrow(exception);
when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());
- 如果被mock的方法有参数值,你很确定在测试的时候这个值是什么的话,可以直接写这个值或者用eq。
- 如果被mock的方法有参数值,但是不确定或者不在乎测试的时候这个值是什么的话,可以用
any()
,any(YourClass.class)
或者是anyString()
和anyList()
这种带具体类型的Matchers
。 - 如果被mock的方法有参数值,不确定参数的具体值,但是有一个的规则,则可以用一些条件匹配。例如,
isNull
,isNotNull
,notNull
,contains(String substring)
,startsWith(String prefix)
,endsWith(String suffix)
,matches(String regex)
。或者用argThat(Matcher<T> matcher)
来写一些你自定义的匹配方法。 - 当mock的方法的返回值需要用户自定义且比较复杂的时候,可以用
thenAnswer
来写一个answer方法来根据参数确定返回值,例如对dependencyYY.doSomethingForY()
的mock可以用thenAnswer
改写成下面的样子。
Map<String, String> mapYToSomething = new HashMap<String, String>() {{
put("B", "str1");
put("A", "str2");
put("C", "str3");
}};
when(dependencyYY.doSomethingForY(anyString())).thenAnswer(new Answer<String>() {
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return mapYToSomething.get(args[0]);
}
});
4.2 mock被测对象中的成员没有返回值的方法
当被测对象依赖外部dependency的方式是一个没有返回值的方法时,我们在测试的时候并不希望这个方法被真正的调用,例如dependencyZ.sendNotification(message)
,但是我们依旧希望验证的是这个方法在确实被调用了。这个时候需要用verify
语句对它们进行验证。
verify(mockedClass [, times(x), timeout(t)]).method(params);
verify(dependencyZ, times(0)).sendNotification(anyString());
verify(dependencyZ, never()).sendNotification(anyString());
verify(dependencyZ).sendNotification(anyString());
verify(dependencyZ, times(1)).sendNotification(anyString());
verify(calcService, timeout(100)).add(20.0,10.0);
和上面when
语句里方法的参数一样,这里的参数列表也可以写一个确定的值,或者任意值,或者匹配一定条件的值。
- Capturing arguments
当这里的参数值很复杂,我们想写一个合适的Matcher
很复杂的时候,也可以用Capturing技术先把参数放进一个Captor
中,然后再对其中的值进行验证,例如,上面例子里的notificationCaptor
。
@Captor
private ArgumentCaptor<String> notificationCaptor;
verify(dependencyZ).sendNotification(notificationCaptor.capture());
Assert.assertTrue(notificationCaptor.getValue().contains("Error"));
注意,虽然我把verify放在这个section讲,但是并不表示它只能用来验证返回值为空的方法,有返回值的方法同样适用。只是一般有返回值的方法我们会mock它的返回值,如果它确实拿到那个mocked值,那说明它执行了,就没必要再做多余的验证了。
- Verify methods in order
另外,如果我们对被mock对象的方法的执行顺序有要求的时候,可以先定义一个所mock对象的InOrder
,然后用InOrder.verify()
依你期望的顺序去验证它的一些方法。
InOrder inOrder = inOrder(calcService);
inOrder.verify(calcService).add(20.0,10.0);
inOrder.verify(calcService).subtract(20.0,10.0);
4.3 Partial Mocking
虽然Mockito支持用Spy
去partial mock一些真实的instance,但是使用它的时候还是需要谨慎。对于spy
的mock,最好使用doReturn|Answer|Throw()
这类方法。
List list = new LinkedList();
List spy = spy(list);
// Impossible: real method is called so spy.get(0)
// throws IndexOutOfBoundsException (the list is yet empty)
when(spy.get(0)).thenReturn("foo");
// You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);
还需要注意的是,Mockito的spy
只是copy了你所mock的那个真实的instance,你之后的交互应该跟这个copy的spying instance交互,它不能检测到任何跟原来的real instance的交互。例如,上面的List有两个instance,一个是真实创建出来的LinkedList: list
,另一个就是带有mock的spy(list)
。之后如果我们对list
进行操作,spy
是不知情的。
此外,对于spy
的object不要去试图mock final方法。
Watch out for final methods. Mockito doesn't mock final methods so the bottom line is: when you spy on real objects + you try to stub a final method = trouble. Also you won't be able to verify those method as well.
4.4 mock在被测对象中的直接new出来的对象
当我们需要mock那些直接在被测方法中new
出来的对象的时候,直接在单元测试中用Mock还做不到JMockit的Mocked那么强大,比如说上面例子中的dependencyX
。我们的做法是用PowerMockito去mock这个类的构造函数,返回一个我们mock的instance。关于PowerMockito的更多信息,我们将在下一节中介绍。
需要注意的是,当使用PowerMockito.whenNew()的的时候需要在PrepareForTest里加上需要new ClassA的被测试的ClassB。例如上面的例子,我们在DemoService类里直接调用new DependencyX()来创建一个DependencyX的实例,我们需要prepare的不是DependencyX而是DemoService。
@Mock
private DependencyX dependencyX;
PowerMockito.whenNew(DependencyX.class).withAnyArguments()
.thenReturn(dependencyX);
when(dependencyX.getX()).thenReturn("str2");
另外,当需要验证被测试的逻辑中有构造某个对象,则可以用PowerMockito.verifyNew
。
PowerMockito.verifyNew(MyClass.class).withNoArguments();
4.5 mock static方法
对于静态方法的mock,我们就需要用到PowerMock了。
(0)使用PowerMockito的时候需要在单元测试类上添加annotation @RunWith(PowerMockRunner.class)
和@PrepareForTest({YourStaticClass.class})
。
为什么我们需要PrepareForTest呢,这里是官方文档里的介绍。总之,就是告诉PowerMock那些测试中需要mock的类,尤其是那些final类和需要mock私有的,静态的或者native的方法的类。
This annotation tells PowerMock to prepare certain classes for testing. Classes needed to be defined using this annotation are typically those that needs to be byte-code manipulated. This includes final classes, classes with final, private, static or native methods that should be mocked and also classes that should be return a mock object upon instantiation.
当使用这个PrepareForTest
的时候,我们还要么加上@RunWith(PowerMockRunner.class)
,要么用下面的语句来初始化prepare的那些需要mock的东西。
public static TestSuite suite() throws Exception {
return new PowerMockSuite(MyTestCase.class);
}
(1)Mock一个static类和它的方法,例如上面的StaticDependency. doAnything()
。
PowerMockito.mockStatic(StaticClass.class);
Mockito.when(StaticClass.staticMethod(param)).thenReturn(value);
(2)验证mock对象的行为。与上面一样,也需要先声明一下verifyStatic
,然后直接验证具体的方法,连verify()
都不需要了。如果方法有参数列表,则对参数的匹配可以用Mockito.Matchers
那一套。注意,这两步是验证一个static方法必不可少的。当需要验证多个static的方法时,每个方法都需要加上verifyStatic()
。
PowerMockito.verifyStatic(StaticClass.class);
StaticClass.staticMethod(param);
(3)mock static的方法抛出异常。对于有返回值的方法,也可以直接用when/thenThrow
,对于没有返回值的方法,则需要用doThrow/when(staticClass)
,然后再写相应的方法。
PowerMockito.doThrow(new RuntimeException()).when(StaticDependency.isEnabled());
PowerMockito.doThrow(new RuntimeException()).when(StaticDependency.class);
StaticDependency.doAnything();
PowerMockito.doThrow(new RuntimeException()).when(myFinalMock).myFinalMethod();
(4)mock和verify私有的方法。对于私有的方法,我们直接用mockedClass.是无法访问到的,可以用下面这种方式。
PowerMockito.when(tested, "privateMethodName", argument).thenReturn(value);
PowerMockito.verifyPrivate(tested).invoke("privateMethodName", argument);
(5)跟Mockito类似,我们可以用PowerMockito.spy来实现Partial Mocking,下面是从powermock wiki拷贝的一个完整的例子。
@RunWith(PowerMockRunner.class)
// We prepare PartialMockClass for test because it's final or we need to mock private or static methods
@PrepareForTest(PartialMockClass.class)
public class YourTestCase {
@Test
public void spyingWithPowerMock() {
PartialMockClass classUnderTest = PowerMockito.spy(new PartialMockClass());
// use Mockito to set up your expectation
Mockito.when(classUnderTest.methodToMock()).thenReturn(value);
// execute your test
classUnderTest.execute();
// Use Mockito.verify() to verify result
Mockito.verify(mockObj, times(2)).methodToMock();
}
}
References
Mockito Mockito-Core 2.7.12
Tutorialspoint Mockito Overview
Why is Mockito voted better than JMockit
Mockito vs. EasyMock vs. JMockit
Slant: JMockit vs. Mockito
Mockito vs. JMockit
Forming Mockito “grammars”
Parameterized testing with Mockito by using JUnit @Rule
Junit MockitoRule
PowerMock for Mockito
PowerMock Annotation PrepareForTest