概述
介绍
- Mockito 是一种 Java Mock 框架,主要是用来做 Mock 测试,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,避免为了测试一个方法,却要自行构建整个 bean 的依赖链。
同时 Mockito 也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。
- 开发中有些依赖的接口还没有开发完成、有些接口还调不通等情况,但又想要验证部分代码逻辑时,可以使用 Mockito 对接口进行 mock。
mock 可以理解为一个模拟对象,即一个替代者,可以替换掉依赖的对象,这样一来就可以把注意力集中在业务代码逻辑,验证代码的正确性。
- 使用 mock 的好处是,可以对方法的入参和返回值做灵活的设置。
比如可以在初始化的时候设置某个方法入参和返回值,这样当单元测试执行到这个方法的时候就不用受到方法依赖的约束。
假如这样一种情况,想要测定时任务发邮件,其实想测的是这个定时任务逻辑是否正常,对发送邮件是否成功并不关心。由于发送邮件需要依赖其他系统,单元测试是发不了邮件的。这样把发送邮件的方法给 mock 掉就很有必要了。
- 目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前内建的是 Mockito 框架。
依赖引入
方式1:spring-boot-starter-test 模块集成了 mockito
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
方式2:单独引入 mockito 依赖
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.3.3</version> <scope>test</scope> </dependency>
Mockito 的局限性
主要是两个:
- 不能 mock 静态方法
- 不能 mock 私有方法
- 不能 mock final class
这两种方法可能只能是通过其他工具,或者通过上层方法调用来做测试。
快速入门
Mockito 的初始化
当要使用注解(比如 @Mock)来 mock 对象的使用,就需要先初始化 Mockito,这样用 @Mock 标注的对象才会被实例化,否则直接使用会报 Null 指针异常。
有两种初始化的方法:
方式1:使用 MockitoAnnotations.initMocks() 方法
@Mock private List mockList; @Before public void init(){ MockitoAnnotations.initMocks(this); } @Test public void test(){ mockList.add(1); verify(mockList).add(1); }
方式2:在类上标注 @RunWith(MockitoJUnitRunner.class) 注解
@RunWith(MockitoJUnitRunner.class) public class MockitoExample { @Mock private List mockList; @Test public void test(){ mockList.add(1); verify(mockList).add(1); } }
Mock 的使用
mock 主要功能就是模拟一个对象出来,注意 Mock 出来的对象是假对象,对其任何方法的操作都不会真正执行的。
mock 模拟的对象可以理解为真实方法的一个代理,每次对方法的调用其实都是调用了代理方法,这个代理方法是一个空方法,不会做任何事情。方法的返回值都返回默认的:
- boolean:返回 false
- 基本数值类型:返回 0
- 对象类型:返回 null
mock 有两种使用方法:
直接代码 mock 一个对象
// 直接代码mock一个List对象 List list = Mockito.mock(ArrayList.class); // 这里的add操作其实没有真实调用 list.add("22"); // 打印其大小为0 System.out.println(list.size()); // 校验add("22")方法是否执行了,校验通过 Mockito.verify(list).add("22");
用 @Mock 造一个对象
@Mock private List mockList;
使用示例:
public class MockExample {
@Mock
private Service01 service01;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
// 如果加了这个返回值的设置,任何入参调用sendMail方法都是返回false
Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenReturn(false);
}
@Test
public void testMock(){
// 这里直接返回了false,不会进去 sendMail 的代码逻辑
boolean isSendSucc = service01.sendMail("张三","李四");
}
}
public class Service01 {
public boolean sendMail(String sender, String receiver){
System.out.println(sender + "向" + receiver + "发送了一封邮件");
return true;
}
}
调用 mock 对象的方法
指定 mock 方法的返回值
常用大概有以下几种:
方式1:调用完方法后指定返回值
// 格式: Mockito.when(调用的类.方法).thenReturn(指定的返回值); // 示例: Mockito.when(service01.sendMail(Mockito.anyString(), Mockito.anyString())).thenReturn(false);
如果是 @Mock 标注的对象方法,这样设置后不会进去方法执行,直接返回指定值。
如果是 @Spy 标注的对象方法,这样设置后会进去执行方法,但是返回指定的返回值。
注意:
指定方法的返回值时,入参的设置可以使用 matcher 做匹配,其中的 any 方法匹配任意值
// 示例1:当使用任何整数值调用 userService 的 getUser() 方法时,就回传一个自定义 User 对象 Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "I'm mock")); User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock User user2 = userService.getUserById(200); // 回传的user的名字也为I'm mock // 示例2:限制只有当参数的数字是 3 时,才会回传名字为 I'm mock 3 的 user 对象 Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "I'm mock")); User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock User user2 = userService.getUserById(200); // 回传的user为null // 示例3:当调用 userService 的 insertUser() 方法时,不管传进来的 user 是什么,都回传 100 Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100); Integer i = userService.insertUser(new User()); //会返回100
有多个入参的方法,一个入参使用了 matcher 做匹配,那么其他入参也要用 matcher 匹配。
例如下面用了 any 方法,那么第二入参就不能写死了,可以用 eq 方法来做匹配
// 错误的写法: Mockito.when(service01.sendMail(Mockito.anyString(), "王五")).thenReturn(true); // 正确的写法: Mockito.when(service01.sendMail(Mockito.anyString(), Mockito.eq("王五"))).thenReturn(true);
方式2:直接返回指定值
// 格式: Mockito.doReturn(方法返回值).when(spy标注的对象).调用的方法; // 示例: Mockito.doReturn(false).when(service01).sendMail(Mockito.anyString(), Mockito.anyString());
这样设置后,不管是 @Mock 还是 @Spy 标注的对象方法都不会进去执行,会直接返回指定值。
方式3:设置抛出异常
// 格式: Mockito.when(调用方法).thenThrow(抛出的异常类); // 示例: Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenThrow(RuntimeException.class);
mock 返回值类型为 void 的方法
可以用 Mockito 的 doNothing()、doThrow() 和 doAnswer() 来对无返回值的函数进行 Mock 和验证。
doNothing()
// 在 Mockito 中 doNothing() 是对无返回值的方法 mock 的默认行为。 @Test public void whenAddCalledVerfied() { MyList myList = mock(MyList.class); doNothing().when(myList).add(isA(Integer.class), isA(String.class)); myList.add(0, ""); verify(myList, times(1)).add(0, ""); } // 与上面的代码等价 @Test public void whenAddCalledVerfied() { MyList myList = mock(MyList.class); myList(0, ""); verify(myList, times(1)).add(0, ""); }
doThrow()
@Test(expected = Exception.class) public void givenNull_AddThrows() { MyList myList = mock(MyList.class); doThrow().when(myList).add(isA(Integer.class), isNull()); myList.add(0, null); }
doAnswer()
@Test public void whenAddCalledAnswered() { MyList myList = mock(MyList.class); doAnswer((Answer) invocation -> { Object arg0 = invocation.getArgument(0); Object arg1 = invocation.getArgument(1); assertEquals(3, arg0); assertEquals("answer me", arg1); return null; }).when(myList).add(any(Integer.class), any(String.class)); myList.add(3, "answer me"); }
Mockito 的 doCallRealMethod() 方法可以用在 void 函数上,允许调用原始对象的实际方法同时也允许去调用验证。
@Test public void whenAddCalledRealMethodCalled() { MyList myList = mock(MyList.class); doCallRealMethod().when(myList).add(any(Integer.class), any(String.class)); myList.add(1, "real"); verify(myList, times(1)).add(1, "real"); }
Spy 的使用
Spy 跟 Mock 不同之处在于,它是会真正执行方法逻辑的。相同之处是它可以指定方法的返回值。
有两种使用方法:
直接代码实例化
@Test public void mock2(){ // 直接代码spy一个List对象 List list = Mockito.spy(ArrayList.class); // 这里的add操作有真实调用 list.add("22"); // 打印其大小为1,是真实调用了的 System.out.println(list.size()); // 校验add("22")方法是否执行了,校验会通过 Mockito.verify(list).add("22"); }
用 @Spy 标注
注意:@Spy 不能标注接口,可以是实现类和抽象类
@Spy private List spyList;
使用示例
@Spy
private Service01 service01;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
// 如果加了这个返回值的设置,那么方法不会执行,任何入参调用sendMail方法都是返回false
// Mockito.doReturn(false).when(service01).sendMail(Mockito.anyString(),Mockito.anyString());
}
@Test
public void testMock(){
// 这里会执行sendMail方法
boolean isSendSucc = service01.sendMail("张三","李四");
System.out.println("邮件发送是否成功:" + isSendSucc);
}
/**
* 单元测试执行结果:
张三向李四发送了一封邮件
邮件发送是否成功:true
*/
InjectMocks 的使用
@InjectMocks 用来给标注的成员变量填充带有 @Mock 和 @Spy 标签的 bean,可以理解为它会吸取所有 @Mock 和 @Spy 标注的bean 为自己所用。
示例1:
bookService 用了 @IninjectMocks,那么 bookService 里面成员变量 bookDao 就会使用 @Mock 标注的 bookDao。
这样就解决了 bean 的依赖问题了,bookService 里面的 bookDao 的任何操作完全可以在单元测试类里指定返回值。
// 测试类
@RunWith(MockitoJUnitRunner.class)
public class BookControllerTest {
@InjectMocks
private BookServiceImpl bookService;
@Mock
private BookDao bookDao;
@Before
public void init(){
Mockito.when(bookDao.getBookById("123")).thenReturn("《java语言》");
}
@Test
public void testGetBook(){
System.out.println(bookService.getBookById("123"));
}
}
// service 接口
public interface BookService {
String getBookById(String id);
}
// 接口实现类
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public String getBookById(String id) {
return bookDao.getBookById(id);
}
}
// dao 接口
@Mapper
public interface BookDao {
String getBookById(String id);
}
示例2:
如果是嵌套的 bean 可以用 ReflectionTestUtils.setFileld() 绑定成员变量。
例如要测试 service 上层的 Controller,希望 mock bookDao,而不是 service:
// 测试类
@RunWith(MockitoJUnitRunner.class)
public class BookControllerTest {
@Spy
private BookServiceImpl bookService;
@InjectMocks
private BookController bookController;
@Mock
private BookDao bookDao;
@Before
public void init(){
Mockito.when(bookDao.getBookById("123")).thenReturn("《java语言》");
// 这里指定bookService的成员变量bookDao
ReflectionTestUtils.setField(bookService,"bookDao",bookDao);
}
@Test
public void testGetBook(){
System.out.println(bookController.getBookById("123"));
}
}
// Controller
@RestController
public class BookController {
@Autowired
private BookService bookService;
@GetMapping("/getBook")
public String getBookById(@RequestParam String id){
return bookService.getBookById(id);
}
}
@MockBean 的使用
@MockBean 是 SpringBoot 中增加的,用来支持容器中的 mock 测试。它跟 mock 的使用逻辑是一样,只是它修饰的对象是容器中的对象,也就是 bean 对象。
// 需要加载SpringBoot的上下文
@RunWith(SpringRunner.class)
@SpringBootTest
public class MockBeanExample {
// 使用容器中对象,用MockBean
@MockBean
private Service01 service01;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
// 如果加了这个返回值的设置,任何入参调用sendMail方法都是返回false
// Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())).thenReturn(false);
}
@Test
public void testMock(){
// 这里直接返回了true,会执行sendMail的代码
boolean isSendSucc = service01.sendMail("张三","李四");
}
}
// 注意这个类是容器中的组件
@Component
public class Service01 {
public boolean sendMail(String sender, String receiver){
System.out.println(sender + "向" + receiver + "发送了一封邮件");
return true;
}
}
@SpyBean 的使用
@SpyBean 也是 SpringBoot 增加的一个注解,用来支持 Spring 容器的单元测试,它与 Spy 的逻辑基本一致,不同之处就在于它标注的对象是容器对象。具体使用可以参考上面 @MockBean 的使用方法。
方法的校验和断言
通常写单元测试就是要断言方法的执行是否符合预期,除了 junit 提供的 Assert 类中的方法外,Mockito 也提供了几种校验方法:
方法1:Mockito.verify() 方法断言方法是否被调用过
// 格式: Mockito.verify(对象).对象的方法; // 示例1:校验list对象是否调用了add(“22”)方法 Mockito.verify(list).add(“22”); // 示例2:检查调用 userService 的 getUserById()、且参数为3的次数是否为1次 Mockito.verify(userService, Mockito.times(1)).getUserById(Mockito.eq(3)) ; // 示例3:验证调用顺序,验证 userService 是否先调用 getUserById() 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用insertUser() 方法 InOrder inOrder = Mockito.inOrder(userService); inOrder.verify(userService).getUserById(3); inOrder.verify(userService).getUserById(5); inOrder.verify(userService).insertUser(Mockito.any(User.class));
方法2:断言异常
@Before public void init(){ MockitoAnnotations.initMocks(this); // 让方法抛出异常 Mockito.when(service01.sendMail(Mockito.anyString(),Mockito.anyString())) .thenThrow(RuntimeException.class); } // 必须抛出指定的异常才会通过测试 @Test(expected=RuntimeException.class) public void testThrowException(){ service01.sendMail("张三","李四"); }
- 方法3:Assert 类中的断言方法
测试 Controller
这里总结了 2 种测试方法,都要在 SpringBoot 的框架中测试。一般在集成测试中使用,需要启动 Spring 容器。
方式1:使用 @AutoConfigureMockMvc(推荐)
@AutoConfigureMockMvc 会自动注入 MockMvc,可以方便的指定入参或者是 header
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class BookControllerTest2 { @Autowired public MockMvc mockMvc; @Test public void testGetBookInfo() throws Exception { MvcResult result = mockMvc.perform( MockMvcRequestBuilders.post("/getBookInfo2").param("id","123").header("user","xiaoming")) .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn(); System.out.println(result.getResponse().getContentAsString()); } } @RestController public class BookController2 { @PostMapping("/getBookInfo2") public String getBookById(@RequestParam String id, @RequestHeader String user){ System.out.println(user + "查询书籍信息,bookId=" + id); return "《java语言》"; } }
方式2:使用 TestRestTemplate 模板
@RunWith(SpringRunner.class) //指定web环境,随机端口 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class BookControllerTest { //这个对象是运行在web环境的时候加载到spring容器中 @Autowired private TestRestTemplate testRestTemplate; @Test public void testGetBookInfo(){ String result = testRestTemplate.getForObject("/getBookInfo?id=123456", String.class); System.out.println(result); } } @RestController public class BookController2 { @GetMapping("/getBookInfo") public String getBookById(@RequestParam String id){ System.out.println("查询书籍信息,bookId=" + id); return "《java语言》"; } }
拓展
@RunWith 的使用
常见用法有以下几种:
@RunWith(SpringRunner.class) 或者 @RunWith(SpringJUnit4ClassRunner.class):
代表在 Spring 容器中运行单元测试。如果配合 @SpringBootTest 就是在 SpringBoot 容器中运行单元测试。
注:SpringRunner 就是 SpringJUnit4ClassRunner 的别名,它们作用是一样的。
@SpringBootTest(classes = MyApplication.class) // classes 加载启动类 @RunWith(SpringRunner.class) public class BaseTest { public void runUnitTest(){ } }
- @RunWith(MockitoJUnitRunner.class)
可以理解为使用 Mockito工作运行单元测试,它会初始化 @Mock 和 @Spy 标注的成员变量
@RunWith(Suite.class):代表是一个集合测试类,一般是如下用法,也就是其可一次性测试多个用例
@RunWith(Suite.class) @Suite.SuiteClasses({ServiceTest.class, A.class}) public class AllTest { }
public class ServiceTest{ @Test public void test01(){ } @Test public void test02(){} }