前言
上次整理了单测的学习笔记,所以这次就对java常见的两个单测工具包(mockito、powerMock)进行学习了解,总结一些常见的用法
mockito
1.mock 对象
Person xiaoHong = new Person();
Person xiaoMing = Mockito.mock(Person.class);
xiaoMing 被加上了一层动态代理 MockMethodInterceptor
2.mock bean 并注入到测试类
使用 @RunWith(MockitoJUnitRunner.class)
@RunWith(MockitoJUnitRunner.class)
public class MockBeanTest {
@Mock
PersonCheckService personCheckService;
@InjectMocks
HospitalController hospitalController;
@Test
public void mockBeanTest(){
}
}
使用 MockitoAnnotations.openMocks();
public class MockBeanTest {
@Mock
PersonCheckService personCheckService;
@InjectMocks
HospitalController hospitalController;
@Before
public void init() {
MockitoAnnotations.openMocks(this);
// 或 MockitoAnnotations.openMocks(MockBeanTest.class);
}
@Test
public void mockBeanTest(){
}
}
手动mock(一般来说不用手动mock,需要写构造函数或者set方法)
public class MockBeanTest {
PersonCheckService personCheckService;
HospitalController hospitalController;
@Before
public void init() {
hospitalController = new HospitalController();
personCheckService = Mockito.mock(PersonCheckService.class);
hospitalController.setPersonCheckService(personCheckService);
}
@Test
public void mockBeanTest(){
}
}
3.验证是否执行了某些操作
Mockito.verify 用于验证方法在给定的方法参数下是否被调用
@Test
public void verifyTest(){
Person xiaoHong = new Person("XiaoHong", 20, 160.0, 50.0);
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
hospitalController.check(xiaoMing);
Mockito.verify(personCheckService).checkPerson(xiaoMing); // √
Mockito.verify(personCheckService).checkPerson(xiaoHong); // ×
}
verify 默认验证的方法只会执行一次,如果将上面用例改成下面这样,就会发生报错
@Test
public void verifyTest(){
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
hospitalController.check(xiaoMing);
hospitalController.check(xiaoMing);
Mockito.verify(personCheckService).checkPerson(xiaoMing);// ×
}
对于特殊的情况,可以对 verify 进行验证模式的配置
例如将上面用例更改验证模式为 times(2),代表会执行2次
Mockito.verify(personCheckService, times(2)).checkPerson(xiaoMing);// √
相关的验证模式选择还有很多:
验证模式 | VerificationMode |
---|---|
行为确切发生n次 | times(n) |
行为从不发生 | never() |
行为至少发生一次 | atLeastOnce() |
行为至少发生n次 | atLeast(n) |
行为最多发生n次 | atMost(n) |
除了对 verify 模式进行指定之外,Mockito 提供一些常见的验证行为的方法
- 验证没有任何交互,即没有调用到 mock 对象的任意一个方法
@Test
public void verifyNoInteractionsTest() {
// 此处直接 mock 了传入的对象用于测试,实际上应该会对测试类中被 mock 的类对象进行 verifyNoInteractions
Person xiaoHong = mock(Person.class);
Person xiaoMing = mock(Person.class);
hospitalController.check(xiaoMing);
Mockito.verifyNoInteractions(xiaoHong); // √
Mockito.verifyNoInteractions(xiaoHong,xiaoMing); // ×
}
- 如果我们需要保证除了我们 verify 的方法调用之外,没有别的冗余的交互时,以上的验证方法就比较难实现了,就需要使用到下面示例
@Test
public void verifyNoMoreInteractionsTest() {
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
hospitalController.check(xiaoMing);
Mockito.verify(personCheckService, times(1)).checkPerson(xiaoMing); // √
Mockito.verifyNoMoreInteractions(personCheckService); // √
hospitalController.get(xiaoMing);
Mockito.verifyNoMoreInteractions(personCheckService); // × 因为上面的操作没有被 verify ,归为冗余操作
}
4.打桩(打桩为测试注入了灵魂)
通过打桩,可以让被我们mock的对象,在调用指定方法的时候,按照我们的需要返回。
这里还是多写一下关于打桩。
打桩的目的主要有:隔离、补齐、控制。
-
隔离
单测的时候需要保证我们测试的单元一定要独立,不能依赖于外部的方法或接口,单元中调用到的方法也需要有对应的单测来保证功能。
-
补齐
如果我们依赖的外部方法还没开发,或者是项目是并行开发,那此时打桩就可以把未完成的方法给代替掉。
-
控制
测试时,我们对外部方法的实现细节不需要去理解,就像个黑盒,但外部方法的返回结果可能跟我们测试结果有直接关系,那此时我们可以通过打桩,控制外部方法按要求返回我们希望的结果。
前两点在我们 mock 对象时已经已经能够解决,例如以下示例,得到结果并不会是真实的返回结果,而是 mock 对象的方法所提供的默认结果,默认值为 null 或是原始/原始包装器值。
@Test
public void stubTest(){
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
Double result = hospitalController.get(xiaoMing); // 0.0
}
通过打桩,我们就是要实现对mock的类方法自定义返回结果
@Test
public void stubTest(){
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
when(personCheckService.getBMIOfPerson(xiaoMing)).thenReturn(20.0);
Double result = hospitalController.get(xiaoMing); // 20.0
}
以上用例使用到的when().thenReturn()是最常用的打桩方式,除此之外,如果对于 void 方法,我们要进行打桩的话(例如我们需要让方法在传入某些参数时抛出异常,或者我们要获取到方法参数等情况),就需要使用到
@Test
public void stubTest(){
Person xiaoHong = new Person("XiaoHong", 20, 160.0, 50.0);
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
// doNothing().when(personCheckService).checkPerson(xiaoMing); // 不作方法参数捕获的话,可以不用特地对 void 方法打桩
doThrow(new RuntimeException()).when(personCheckService).checkPerson(xiaoMing); // 当传入参数为 xiaoMing 时会抛出 RuntimeException 异常
hospitalController.check(xiaoHong); // 不会抛出异常
hospitalController.check(xiaoMing); // 抛出异常
}
5.参数匹配器
如果只是用到上述打桩方式,一定程度上来说是不够灵活的,如果我们方法传入的参数多了,一个个配置固定的值去实现打桩就不太现实,所以也就有了参数匹配器,来使得测试更加自如。
例如刚刚的例子,我们现在需要的是 只要 personCheckService.checkPerson 被调用到了,就抛出一个 RuntimeException
@Test
public void anyTest(){
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
Person xiaoHong = new Person("XiaoHong", 20, 160.0, 50.0);
doThrow(new RuntimeException()).when(personCheckService).checkPerson(any()); // 此处用到是参数匹配器 any()
// 以下两个调用单独执行均会抛出 RuntimeException
hospitalController.check(xiaoHong);
hospitalController.check(xiaoMing);
}
相关的参数匹配器还有
参数匹配器 | 作用 |
---|---|
any() | 匹配任意类对象,包括 null 与 可变参数 |
anyObject() | 匹配任意类对象,包括 null |
any(Class type) | 匹配给定的类T的任意对象 |
isA(Class type) | 匹配实现于类T的所有类对象 |
anyBoolean、anyByte、anyChar、anyString… | 同理与any(Class type),只不过把类型写好了 |
6.验证升级 - 执行顺序的验证
@Test
public void verifyOrderTest() {
Person xiaoHong = new Person("XiaoHong", 20, 160.0, 50.0);
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
hospitalController.check(xiaoMing);
hospitalController.check(xiaoHong);
InOrder inOrder = Mockito.inOrder(personCheckService);
inOrder.verify(personCheckService).checkPerson(xiaoMing);
inOrder.verify(personCheckService).checkPerson(xiaoHong);
}
上面的示例中,如果将两个 inOrder.verify 进行调换,因为执行顺序错误,自然也会验证失败。
开头写 verify 的时候,有写到 verify 默认执行次数为 times 为 1,所以如果如下示例,重复执行两次 check(xiaoMing) ,并在 check(xiaoHong) 之后再执行一次 check(xiaoMing)
@Test
public void verifyOrderTest() {
Person xiaoHong = new Person("XiaoHong", 20, 160.0, 50.0);
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
hospitalController.check(xiaoMing);
hospitalController.check(xiaoMing);
hospitalController.check(xiaoHong);
hospitalController.check(xiaoMing);
InOrder inOrder = Mockito.inOrder(personCheckService);
inOrder.verify(personCheckService, times(2)).checkPerson(xiaoMing); // 此处不能分开写
inOrder.verify(personCheckService).checkPerson(xiaoHong);
inOrder.verify(personCheckService).checkPerson(xiaoMing); // 由于是验证顺序,所以不能计算次数到前面的 verify
}
7.断言
这个就并非是 mockito 组件中的,而是 junit。但这里就一并介绍一下
断言的实现就非常简单,其实只不过是帮我们将 if…else… 给做了,主要是使得代码简洁。
@Test
public void AssertTest() {
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
when(personCheckService.getBMIOfPerson(any(Person.class))).thenReturn(20.0);
Double valueXiaoMing = hospitalController.get(xiaoMing);
Assert.assertEquals("value is error", Double.valueOf(20.0), valueXiaoMing);
}
断言的其他方法(具体方法参数省略,重写的方法有点多)还有
方法 | 说明 |
---|---|
assertTrue | 断言给定的值为 true |
assertFalse | 断言给定的值为 false |
fail | 执行该断言时直接抛出错误 |
assertEquals/assertNotEquals | 断言给定的两个值相等/不相等 |
assertArrayEquals | 断言给定的两个数组相等 |
assertNotNull | 断言给定的值不为 null |
assertSame/assertNotSame | 断言两个对象引用同一对象/没有引用同一对象 |
… |
8.打桩升级Ⅰ - 连续打桩
像上文写的打桩,打桩后的效果是被打桩的方法固定返回指定值,如果要产生不同的值,那就需要连续打桩了
@Test
public void continueStubTest() {
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
Person xiaoHong = new Person("XiaoHong", 20, 160.0, 50.0);
when(personCheckService.getBMIOfPerson(any(Person.class))).thenReturn(20.0,21.0);
Double valueXiaoMing = hospitalController.get(xiaoMing); // 20.0
Double valueXiaoHong = hospitalController.get(xiaoHong); // 21.0
}
当执行次数超过连续打桩的数量时,返回连续打桩的最后一个值。
9.打桩升级Ⅱ - 获取方法传入参数
使用到了 ArgumentCaptor 指定我们要获得的方法参数类型,并在打桩的时候设置在对应位置上,可以看成是设置了 any(Person.class),在打桩功能不变的情况下,ArgumentCaptor 会捕获到对应的参数,如下示例
@Test
public void ArgumentCaptorTest() {
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
ArgumentCaptor<Person> personCaptor = ArgumentCaptor.forClass(Person.class);
when(personCheckService.getBMIOfPerson(personCaptor.capture())).thenReturn(20.0);
Double valueXiaoMing = hospitalController.get(xiaoMing);
Assert.assertEquals("value is error", Double.valueOf(20.0), valueXiaoMing);
Assert.assertEquals("Argument is error", xiaoMing, personCaptor.getValue());
}
10.mock 静态方法
Mockito 3.4.0 已经可以实现mock静态方法,如下示例
@Test
public void mockStaticTest() {
try (MockedStatic<CheckUtil> mockCheckUtil = Mockito.mockStatic(CheckUtil.class)) {
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
mockCheckUtil.when(() -> CheckUtil.getBMIOfPerson(xiaoMing)).thenReturn(10.0);
Double result = personCheckService.getBMIOfPerson(xiaoMing); // 10.0
}
}
但注意,静态方法应不应该mock,其实这是一个挺有争议性的话题,可以看 why-doesnt-mockito-mock-static-methods
到了这一步,已经可以满足日常开发中单测编写的需要了,当然还有挺多点没有细写出来。
powerMock
相比于 mockito,powerMock 在兼容 mockito 的基础上,增加了许多特殊的 mock 操作
也就是上面 mockito 的示例,@RunWith(PowerMockRunner.class) 也可以实现测试,此处就不再展开。
1.powerMock 注解
使用 powerMock 最常用的就是以下4个注解,标注在测试类上
-
@RunWith(PowerMockRunner.class)
表明使用 PowerMockRunner 进行测试
-
@PrepareForTest({})
可以简单理解成,把需要用到 powerMock 进行特殊 mock 操作的类放到该注解中,这些特殊操作会涉及到字节码级别的修改,所以需要提前准备
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.
-
@SuppressStaticInitializationFor("")
可阻止对应类的静态代码块执行
-
@PowerMockIgnore()
表明不使用 powerMock 来加载所声明的 package 路径的类
如果在使用 powerMock 时出现系统类异常,可以尝试将其对应的包加到该注解中
2.mock 静态方法
现在网上搜索 mock 静态方法,十有八九都是清一色推荐 powerMock 的方法,主要是 Mockito 3.4.0 之前确实不支持,其次也是照搬文化盛行。
如果只是单纯需要 mock 静态方法的话,建议还是只使用 mockito 即可
以下是使用 powerMock 的示例
@Test
public void mockStaticTest() {
mockStatic(CheckUtil.class);
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0);
// when(CheckUtil.getBMIOfPerson(xiaoMing)).thenReturn(10.0);
when(CheckUtil.getBMIOfPerson(xiaoMing)).thenAnswer((Answer<Double>) invocation -> 10.0);
Double result = personCheckService.getBMIOfPerson(xiaoMing); // 10.0
}
至于为什么 when…thenReturn… 会报错以及对应的解决方法,暂时我也没有找到,所以这里用 thenAnswer 代替
3.mock 构造函数
@Test
public void mockNewTest() throws Exception {
Person xiaoHong = new Person("XiaoHong", 20, 160.0, 50.0);
PowerMockito.whenNew(Person.class).withAnyArguments().thenReturn(xiaoHong);
Person xiaoMing = new Person("XiaoMing", 20, 180.0, 70.0); // 会变成 xiaoHong 的数据
}
4.mock final 方法
personCheckService 是被 @Mock 标注的,且需要设置 @PrepareForTest({PersonCheckServiceImpl.class})
其他的跟 mock 其他方法一样
@Test
public void mockFinalTest(){
when(personCheckService.getPersonInfo()).thenReturn("ok");
Assert.assertEquals("ok",personCheckService.getPersonInfo());
}
5.mock 私有方法、final 类等等
用法就不做详细说明,大体使用方法一致