Mockito详细教程
Mockito详细教程-CSDN博客
- Mockito 详细教程-CSDN 博客
- https://blog.csdn.net/qq_37855749/article/details/125362496
- 2024-08-09 11:16:40
如果需要初始化一些值的变量,可以通过
@Before public void setUp() { MockitoAnnotations.openMocks(this); ReflectionTestUtils.setField(ratePlanHandler, "sendHandler", sendHandler); ReflectionTestUtils.setField(ratePlanService, "mockOpen", 0); }
前言
单元测试(UT)
工作一段时间后,才真正意识到代码质量的重要性。虽然囫囵吞枣式地开发,表面上看来速度很快,但是给后续的维护与拓展制造了很多隐患。
作为一个想专业但还不专业的程序员,通过构建覆盖率比较高的单元测试用例,可以比较显著地提高代码质量。如后续需求变更、版本迭代时,重新跑一次单元测试即可校验自己的改动是否正确。
Mockito 和单元测试有什么关系?
与集成测试将系统作为一个整体测试不同,单元测试更应该专注于某个类。所以当被测试类与外部类有依赖的时候,尤其是与数据库相关的这种费时且有状态的类,很难做单元测试。但好在可以通过“Mockito”这种仿真框架来模拟这些比较费时的类,从而专注于测试某个类内部的逻辑。
SpringBoot 与 Mockito
spring-boot-starter-test 中已经加入了 Mockito 依赖,所以我们无需手动引入。
另外要注意一点,在 SpringBoot 环境下,我们可能会用 @SpringBootTest 注解。
@Target({ ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({ SpringExtension.class})
public @interface SpringBootTest {
如果用这个注解,跑单元测试的时候会加载 SpringBoot 的上下文,初始化 Spring 容器一次,显得格外的慢,这可能也是很多人放弃在 Spring 环境下使用单元测试的原因之一。
不过我们可以不用这个 Spring 环境,单元测试的目的应该是只测试这一个函数的逻辑正确性,某些容器中的相关依赖可以通过 Mockito 仿真。
所以我们可以直接拓展自 MockitoExtendsion,这样跑测试就很快了。
@ExtendWith(MockitoExtension.class)
public class ListMockTest {
}
基本使用
mock 与 verify
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension;import java.util.List;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
public class ListMockTest {@Test public void mockList() { List mockedList = mock(List.class); mockedList.add("one"); mockedList.clear(); verify(mockedList).add("one"); verify(mockedList).clear(); }
}
mock(List.class)会返回一个 List 的仿真对象,可以理解为“假对象”,要与后面提到的 spy 区分开。
通过 Mockito 的 verify 来验证是否调用过 List 的 add 方法。
stubbing(存根)
什么是存根
注意:mocking 和 stubbing 背后的理论很庞大。这里的解释只是针对于这个框架而言,比较粗浅。
上面通过 mock 函数得到了一个代理对象,调用这个对象的函数时,如果有返回值,默认情况下返回值都是 null,如果基本类型,默认值是 0 或者 false。
@Test public void mockList() { List mockedList = mock(List.class);System.out.println(mockedList.get(0)); }
控制台输出
null
当测试的单元依赖这个 mock 对象的返回值时,我们可以通过提前申明这个函数的返回值来测试各种各样的场景。
提前申明的这个过程被称为存根。
@ExtendWith(MockitoExtension.class) public class ListMockTest {@Test public void mockList() { List mockedList = mock(List.class); //调用get(0)时,返回first when(mockedList.get(0)).thenReturn("first"); //调用get(1)时,直接抛出异常 when(mockedList.get(1)).thenThrow(new RuntimeException()); //返回first System.out.println(mockedList.get(0)); //抛出异常 System.out.println(mockedList.get(1)); //没有存根,则会返回null System.out.println(mockedList.get(999)); }
}
注意点
- 存根时可以被覆盖的(即对一种情况多次存根的话,以最后一次为准),但是不鼓励这么做,可读性会变差。
- 一旦存根后,这个函数会一直返回这个值,不管你调用多少次。
返回值为 void
即使有些函数返回值为 void,也可以使用存根。
//调用clear方法时,抛出异常 // 因为clear方法没有返回值,所以不能在后面接着写return,只能在前面些doThrow或者doReturn doThrow(new RuntimeException()).when(mockedList).clear();
mockedList.clear();
连续存根
多次调用,返回不同的值。
@Test public void mockList() { List mockedList = mock(List.class); when(mockedList.get(0)).thenReturn(0).thenReturn(1).thenReturn(2);System.out.println(mockedList.get(0)); System.out.println(mockedList.get(0)); System.out.println(mockedList.get(0)); }
返回值:
0
1
2
也可以简化为下面的这种写法,效果一样。
when(mockedList.get(0)).thenReturn(0, 1, 2);
设置回调函数
调用某个函数的时候,执行一个回调函数。
@Test public void mockList() { List mockedList = mock(List.class); when(mockedList.get(anyInt())).thenAnswer(new Answer<Object>() { @Override public Object answer(InvocationOnMock invocationOnMock) throws Throwable { System.out.println("哈哈哈,被我逮到了吧"); Object[] arguments = invocationOnMock.getArguments(); System.out.println("参数为:" + Arrays.toString(arguments)); Method method = invocationOnMock.getMethod(); System.out.println("方法名为:" + method.getName());return "结果由我决定"; } }); System.out.println(mockedList.get(0)); }
控制台打印:
哈哈哈,被我逮到了吧
参数为:[0]
方法名为:get
结果由我决定
存根函数家族
除了上面出现的 doReturn、doThrow、doAnswer 外,还有:
doNothing() 啥也不干
doCallRealMethod() 调用真正的方法(不代理)
参数匹配器
基本用法
看完上面的存根,可能会有一个疑问:如果我想监控这个对象有没有被调用 get 方法,具体参数是什么我并不关心,该咋办。
这个时候就用到了参数匹配器。
@Test public void mockList() { List mockedList = mock(List.class);when(mockedList.get(0)).thenReturn("first"); //返回first System.out.println(mockedList.get(0)); //验证是否调用过get函数。这里的anyInt()就是一个参数匹配器。 verify(mockedList).get(anyInt()); }
处理 anyInt(),还有很多的参数匹配器,默认的放在 ArgumentMatchers 类中。当然,也可以根据需求自定义参数匹配器或者使用 hamcrest 匹配器。
当一个函数接收多个参数时,如果其中有一个用了参数匹配器,那其他的参数也必须用。
class Student{ public void sleep(int id, String studNo, String name) {} } @Test public void mockStudent() { Student student = mock(Student.class); student.sleep(1, "1", "admin"); verify(student).sleep(anyInt(), anyString(), eq("admin")); verify(student).sleep(anyInt(), anyString(), eq("admin")); }
正确的用法是:
@Test public void mockStudent() { Student student = mock(Student.class);student.sleep(1, "1", "admin"); verify(student).sleep(anyInt(), anyString(), eq("admin")); }
ArgumentCaptor
当我们需要去验证函数外部的一些参数时,就需要用到这个。
以发送邮件为例
定义一个邮件类:
@Data @NoArgsConstructor public class Email {private String to; private String subject; private String body; private EmailStyle emailStyle; public Email(String to, String subject, String body) { this.to = to; this.subject = subject; this.body = body; }
}
邮件有以下两种样式
public enum EmailStyle {
HTML,DOC;
}
邮件服务会调用邮件平台发送邮件
public class EmailService {private DeliveryPlatform deliveryPlatform; public EmailService(DeliveryPlatform deliveryPlatform) { this.deliveryPlatform = deliveryPlatform; } public void send(String to, String subject, String body, boolean html) { EmailStyle emailStyle = EmailStyle.DOC; if(html) { emailStyle = EmailStyle.HTML; } Email email = new Email(to, subject, body); email.setEmailStyle(emailStyle); deliveryPlatform.deliver(email); }}
邮件平台代码如下:
public class DeliveryPlatform {public void deliver(Email email) { //do something }
}
现在我想验证一个问题,当我发送 HTML 邮件时,deliver 这个函数收到的 email 到底是不是 HTML 类型的。
这种情况下,就可以通过 ArgumentCaptor 的方式来解决了。
@ExtendWith(MockitoExtension.class) public class EmailServiceTest {@Mock private DeliveryPlatform deliveryPlatform; @InjectMocks private EmailService emailService; @Captor private ArgumentCaptor<Email> emailArgumentCaptor; @Test public void testHtmlEmail() { emailService.send("某人", "无题", "无内容", true); verify(deliveryPlatform).deliver(emailArgumentCaptor.capture()); Email email = emailArgumentCaptor.getValue(); Assertions.assertEquals(EmailStyle.HTML, email.getEmailStyle()); }}
验证函数被调用的次数
下面的这个测试将不会通过
@Test public void mockList() { List mockedList = mock(List.class);when(mockedList.get(0)).thenReturn("first"); //返回first System.out.println(mockedList.get(0)); System.out.println(mockedList.get(0)); //验证是否被用过get verify(mockedList).get(anyInt()); }
报错如下:
org.mockito.exceptions.verification.TooManyActualInvocations:
list.get(<any integer>);
Wanted 1 time:
-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:43)
But was 2 times:
-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:39)
-> at com.dayrain.mockitodemo.test.ListMockTest.mockList(ListMockTest.java:40)
大概意思是,只希望这个函数被调用一次,但实际上被调用了两次。
可能有点懵,不过点进 verify 方法后就明白了,默认情况下只调用一次;
public static <T> T verify(T mock) {
return MOCKITO_CORE.verify(mock, times(1));
}
所以在调用的 verify 方法的时候,指定下调用次数即可。
verify(mockedList, times(2)).get(anyInt());
甚至支持不指定固定次数
//一次也不能调用,等于times(0) verify(mockedList, never()).add("never happened");
//至多、至少
verify(mockedList, atMostOnce()).add("once");
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("three times");
verify(mockedList, atMost(5)).add("three times");
创建 mock 对象的另一种方式:@Mock
上述方法都是通过 mock 方法来构建仿真对象的,其实更简单的方法是通过注解。
@Mock private List mockedList;@Test public void mockList() { mockedList.add("one"); verify(mockedList).add("one"); }
Spy(间谍)
介绍
上面讲的一些操作都是和 Mock 出来的对象相关的。通过 mock() 或者 @Mcok 注解标注的对象,可以理解为“假对象”。
如果一个对象使用
@Mock 修饰,那么不会执行该对象的内部真实方法,都是统一直接返回默认值(0,null,false),或者返回我们使用thenReturn 方法执行的返回结果,如果我们想要他能够进入这个对象的方法内部是进行实际执行,那么就使用@Spy 注解即可
Spy 是针对于“真实存在”的对象。
在重构已有的旧代码时,Spy 会比较好用。
@Test public void spyList() { //申请了一个真实的对象 List list = new LinkedList(); List spy = spy(list);//可以选择存根某些函数 when(spy.size()).thenReturn(100); //调用真实的方法 spy.add("one"); spy.add("two"); //打印第一个元素 System.out.println(spy.get(0)); //获取list的大小 System.out.println(spy.size()); //验证 verify(spy).add("one"); verify(spy).add("two"); }
当使用 spy 的时候,有一个很容易掉进去的陷进。即 spy 监听的是真实的对象,在操作真实对象的时候可能会出现越界之类的问题。
@Test public void spyList() { List list = new LinkedList(); List spy = spy(list);//报错 IndexOutOfBoundsException, 因为这个List还是empty when(spy.get(0)).thenReturn("foo"); //通过 doReturn("foo").when(spy).get(0); }
注解
和 @Mock 类似,还可以用 @Spy 注解。
BDD(行为驱动开发)
针对比较流行的行为驱动开发,Mockito 也提供了对应的支持:
如 org.mockito.BDDMockito 类中的 given//when//then
BDD 本文就不做拓展了,后续有时间再做梳理。
超时验证
如果要验证执行是否超时,可以这么做:
verify(student, timeout(1).times(1)).sleep(anyInt(), anyString(), eq("admin"));
自动实例化 @InjectMocks
下面举一个比较常见的例子
已有用户类
@Data public class UserInfo { private String name; private String password;public UserInfo(String name, String password) { this.name = name; this.password = password; }
}
有对应的服务以及数据存储接口
@Service public class UserInfoService {@Autowired private UserInfoDao userInfoDao; public void printInfo() { UserInfo userInfo = userInfoDao.select(); System.out.println(userInfo); }}
public interface UserInfoDao {
UserInfo select();
}
如果我要测试这个 service,并且不想和数据库有交互,那么可以创建一个 UserInfoDao mock 对象。
被测试类标注为@InjectMocks 时,会自动实例化,并且把 @Mock 或者 @Spy 标注过的依赖注入进去。
@ExtendWith(MockitoExtension.class) public class UserInfoServiceTest {@InjectMocks private UserInfoService userInfoService; @Mock private UserInfoDao userInfoDao; @Test public void testPrint() { UserInfo userInfo = new UserInfo("admin", "123"); when(userInfoDao.select()).thenReturn(userInfo); userInfoService.printInfo(); }}
运行结果为:
UserInfo(name=admin, password=123)
参考
本文大部分内容来自于官网,但不会完全照搬,只整理我认为可能用得到的地方。并且可能会用自己的语言重新组织一下,或者替换部分示例代码,望谅解。
官网地址:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
参考博客 1:https://semaphoreci.com/community/tutorials/stubbing-and-mocking-with-mockito-2-and-junit
参考博客 2:https://www.baeldung.com/mockito-argumentcaptor
如果您对其他语言的模拟也比较感兴趣,例如 python,可以学习下面的博客:
https://semaphoreci.com/community/tutorials/getting-started-with-mocking-in-python

浙公网安备 33010602011771号