1. Mock简介
1.1. Mock 的两个概念
1.1.1. Mock 对象
模拟对象的概念就是我们想要创建一个可以替代实际对象的对象,这个模拟对象要可以通过特定参数调用特定的方法,并且能返回预期结果。
1.1.2. Stub(桩)
桩指的是用来替换具体功能的程序段。桩程序可以用来模拟已有程序的行为或是对未完成开发程序的一种临时替代。也就是对调用方法的模拟。
1.2. 使用场景
在使用Mock的过程中,发现Mock是有一些通用性的,对于一些应用场景,是非常适合使用Mock的:
- 真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)
- 真实对象很难被创建(比如具体的web容器)
- 真实对象的某些行为很难触发(比如网络错误)
- 真实情况令程序的运行速度很慢
- 真实对象有用户界面
- 测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)
- 真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)
当然,也有一些不得不Mock的场景:
- 一些比较难构造的Object:这类Object通常有很多依赖,在单元测试中构造出这样类通常花费的成本太大。
- 执行操作的时间较长Object:有一些Object的操作费时,而被测对象依赖于这一个操作的执行结果,例如大文件写操作,数据的更新等等,出于测试的需求,通常将这类操作进行Mock。
- 异常逻辑:一些异常的逻辑往往在正常测试中是很难触发的,通过Mock可以人为的控制触发异常逻辑。
1.3. Mockito资料
- 官网: mockito.org
- API文档:javadoc.io/static/org.…
- 项目源码:github.com/mockito/moc…
API文档很详细
2. Mockito 使用方法
Mockito是最流行的Java mock框架之一.
2.1. Maven包引入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>mockito_learn</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mockito_learn</name>
<description>mockito_learn</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.2. 使用mock方法
package com.mockito.learn.service;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class MockTest {
@Test
public void mockClassTest() {
Random mockRandom = mock(Random.class);
// 默认值: mock 对象的方法的返回值默认都是返回类型的默认值
System.out.println(mockRandom.nextBoolean()); // false
System.out.println(mockRandom.nextInt()); // 0
System.out.println(mockRandom.nextDouble()); // 0.0
// mock: 指定调用 nextInt 方法时,永远返回 100
when(mockRandom.nextInt()).thenReturn(100);
assertEquals(100, mockRandom.nextInt());
assertEquals(100, mockRandom.nextInt());
List mockList = mock(List.class);
// 接口的默认值:和类方法一致,都是默认返回值
assertEquals(0, mockList.size());
assertEquals(null, mockList.get(0));
// 注意:调用 mock 对象的写方法,是没有效果的
mockList.add("a");
assertEquals(0, mockList.size()); // 没有指定 size() 方法返回值,这里结果是默认值
assertEquals(null, mockList.get(0)); // 没有指定 get(0) 返回值,这里结果是默认值
// mock值测试
when(mockList.get(0)).thenReturn("a"); // 指定 get(0)时返回 a
assertEquals(0, mockList.size()); // 没有指定 size() 方法返回值,这里结果是默认值
assertEquals("a", mockList.get(0)); // 因为上面指定了 get(0) 返回 a,所以这里会返回 a
assertEquals(null, mockList.get(1)); // 没有指定 get(1) 返回值,这里结果是默认值
}
@Test
void mock_element_test_01() {
// mock创建一个 LinkedList
LinkedList mockedList01 = mock(LinkedList.class);
// 使用stub,假设mockedList01.get(0)被调用时,会返回"first"
when(mockedList01.get(0)).thenReturn("first");
// 控制台会打印"first"
System.out.println(mockedList01.get(0));
// 控制台会打印"null",因为我们没有假设get(999)的返回值
System.out.println(mockedList01.get(999));
}
@Test
void mock_element_test() {
// 使用 mock 静态方法创建 Mock 对象.
List mockedList = mock(List.class);
assertTrue(mockedList instanceof List);
assertFalse(mockedList.add("two"));
assertNotEquals(mockedList.size(), 1);
// 连续调用,第一次返回"Hello," 第二次返回"Mockito!"
Iterator i = mock(Iterator.class);
when(i.next()).thenReturn("Hello,").thenReturn("Mockito!");
String result = i.next() + " " + i.next();
assertEquals("Hello, Mockito!", result);
}
@Test
void mock_element_test_error() {
// 连续调用,第一次返回"Hello," 第二次返回"Mockito!" 错误写法
Iterator i = mock(Iterator.class);
when(i.next()).thenReturn("Hello,");
when(i.next()).thenReturn("Mockito!");
String result = i.next() + " " + i.next();
System.out.println(result); //Mockito! Mockito!
}
@Test
void stubbing_element_test() {
//You can mock concrete classes, not just interfaces
LinkedList mockedList = mock(LinkedList.class);
//stubbing
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
//following prints "first"
System.out.println(mockedList.get(0));
//抛出异常
//following throws runtime exception
//System.out.println(mockedList.get(1));
//following prints "null" because get(999) was not stubbed
System.out.println(mockedList.get(999));//null
//Although it is possible to verify a stubbed invocation, usually it's just redundant
//If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
//If your code doesn't care what get(0) returns, then it should not be stubbed.
verify(mockedList).get(0);
}
/**
* 验证行为是否发生
*/
@Test
public void mock_element_test_02() {
//模拟创建一个List对象
List mock = mock(List.class);
//使用mock的对象
mock.add(1);
System.out.println(mock); //Mock for List, hashCode: 1616359099
System.out.println(mock.size()); //0
System.out.println(mock.get(0)); //null
mock.clear();
// 使用stubbing,假设mockedList.get(0)被调用时,会返回"first"
when(mock.get(0)).thenReturn("first");
System.out.println(mock.get(0)); //first
// 控制台会打印"null",因为我们没有假设get(999)的返回值
System.out.println(mock.get(999));
// 验证行为mock.get(0)是否发生,成功
verify(mock).clear();
// 验证行为mock.get(1)是否发生,失败
verify(mock).get(1);
}
/**
* 使用stubbing
* stubbing可以被覆写
* <p>
* 默认情况下,对于所有返回值的方法,mock将返回null、基元/基元包装器值或空集合(视情况而定)。例如,对于int/Integer为0,对于布尔值/布尔值为false。
* <p>
* stubbing 可以被重写:该方法将始终返回一个存根值,无论它被调用多少次。
*/
@Test
public void mock_element_test_03() {
// 你可以mock具体的类型,不仅只是接口
LinkedList mockedList = mock(LinkedList.class);
// 测试桩(可以使用连续调用)
//when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(0)).thenReturn("first").thenReturn("second");
when(mockedList.get(1)).thenThrow(new RuntimeException());
// 输出“first”
System.out.println(mockedList.get(0));
// 连续调用则输出“second”
System.out.println(mockedList.get(0));
// 抛出异常
//System.out.println(mockedList.get(1));
// 因为get(999) 没有打桩,因此输出null
System.out.println(mockedList.get(999));
}
/**
* 验证函数的确切、最少、从未调用次数
*/
@Test
public void mock_element_test_04() {
// 你可以mock具体的类型,不仅只是接口
LinkedList mockedList = mock(LinkedList.class);
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// 下面的两个验证函数效果一样,因为verify默认验证的就是times(1)
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
// 验证具体的执行次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
// 使用never()进行验证,never相当于times(0)
verify(mockedList, never()).add("never happened");
// 使用atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
mockedList.add("five times");
mockedList.add("five times");
mockedList.add("five times");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//最少or最多
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");
}
/**
* 模拟异常
*/
@Test
public void mock_element_test_05() {
LinkedList mockedList = mock(LinkedList.class);
doThrow(new ArithmeticException()).when(mockedList).clear();
// 调用mockedList.clear()方法的时候会抛出异常RuntimeException
assertThrows(ArithmeticException.class, mockedList::clear);
// thenThrow 中可以指定多个异常。在调用时异常依次出现。若调用次数超过异常的数量,再次调用时抛出最后一个异常。
Random mockRandom = mock(Random.class);
when(mockRandom.nextInt()).thenThrow(new ArithmeticException("异常1"), new RuntimeException("异常2"));
assertThrows(ArithmeticException.class, mockRandom::nextInt);
assertThrows(RuntimeException.class, mockRandom::nextInt);
// 对于返回类型是 void 的函数,thenThrow 是无效的,要使用 doThrow。
ExampleService exampleService = mock(ExampleService.class);
// 这种写法可以达到效果
doThrow(new RuntimeException("异常")).when(exampleService).hello();
assertThrows(RuntimeException.class, exampleService::hello);
}
static class ExampleService {
public void hello() {
System.out.println("Hello");
}
}
/**
* RETURNS_SMART_NULLS实现了Answer接口的对象,它是创建mock对象时的一个可选参数,mock(Class,Answer)。
* <p>
* 在创建mock对象时,有的方法我们没有进行stubbing,所以调用时会放回Null这样在进行操作是很可能抛出NullPointerException。
* 如果通过RETURNS_SMART_NULLS参数创建的mock对象在没有调用stubbed方法时会返回SmartNull。
* 例如:返回类型是String,会返回"";是int,会返回0;是List,会返回空的List。另外,在控制台窗口中可以看到SmartNull的友好提示。
*/
@Test
public void returnsSmartNullsTest() {
List mock = mock(List.class, RETURNS_SMART_NULLS);
System.out.println(mock.get(0));
//使用RETURNS_SMART_NULLS参数创建的mock对象,不会抛出NullPointerException异常。另外控制台窗口会提示信息“SmartNull returned by unstubbed get() method on mock”
System.out.println(mock.toArray().length);
}
/**
* 参数匹配1
*/
@Test
public void with_arguments() {
Comparable comparable = mock(Comparable.class);
//预设根据不同的参数返回不同的结果
when(comparable.compareTo("Test")).thenReturn(1);
when(comparable.compareTo("Omg")).thenReturn(2);
assertEquals(1, comparable.compareTo("Test"));
assertEquals(2, comparable.compareTo("Omg"));
//对于没有预设的情况会返回默认值
assertEquals(0, comparable.compareTo("Not stub"));
}
/**
* 参数匹配2
*/
@Test
public void with_arguments_01() {
List list = mock(List.class);
// 精确匹配 0
when(list.get(0)).thenReturn("a");
assertEquals("a", list.get(0));
// 模糊匹配
when(list.get(anyInt())).thenReturn("c");
assertEquals("c", list.get(0));
assertEquals("c", list.get(1));
}
private class IsValid implements ArgumentMatcher<List> {
@Override
public boolean matches(List list) {
return list.size() == 1;
}
}
@Test
public void all_arguments_provided_by_matchers() {
Comparator comparator = mock(Comparator.class);
comparator.compare("nihao", "hello");
//如果你使用了参数匹配,那么所有的参数都必须通过matchers来匹配
verify(comparator).compare(anyString(), eq("hello"));
//下面的为无效的参数匹配使用
//verify(comparator).compare(anyString(),"hello");
}
@Test
public void argumentMatchersTest() {
//创建mock对象
List<String> mock = mock(List.class);
//argThat(Matches<T> matcher)方法用来应用自定义的规则,可以传入任何实现Matcher接口的实现类。
when(mock.addAll(argThat(new IsListofTwoElements()))).thenReturn(true);
mock.addAll(Arrays.asList("one", "two", "three"));
//IsListofTwoElements用来匹配size为2的List,因为例子传入List为三个元素,所以此时将失败。
verify(mock).addAll(argThat(new IsListofTwoElements()));
}
class IsListofTwoElements implements ArgumentMatcher<List> {
@Override
public boolean matches(List list) {
return list.size() == 2;
}
}
/**
* 捕获参数来进一步断言
* 较复杂的参数匹配器会降低代码的可读性,有些地方使用参数捕获器更加合适。
*/
@Test
public void capturing_args() {
PersonDao personDao = mock(PersonDao.class);
PersonService personService = new PersonService(personDao);
ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
personService.update(1, "jack");
verify(personDao).update(argument.capture());
assertEquals(1, argument.getValue().getId());
assertEquals("jack", argument.getValue().getName());
}
class Person {
private int id;
private String name;
Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
interface PersonDao {
public void update(Person person);
}
class PersonService {
private PersonDao personDao;
PersonService(PersonDao personDao) {
this.personDao = personDao;
}
public void update(int id, String name) {
personDao.update(new Person(id, name));
}
}
/**
* 使用方法预期回调接口生成期望值(Answer结构)
*/
@Test
public void answerTest() {
//创建mock对象
List<String> mockList = mock(List.class);
when(mockList.get(anyInt())).thenAnswer(new CustomAnswer());
assertEquals("hello world:0", mockList.get(0));
assertEquals("hello world:999", mockList.get(999));
}
private class CustomAnswer implements Answer<String> {
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return "hello world:" + args[0];
}
}
}
2.3 使用@Mock注解
@Mock 注解可以理解为对 mock 方法的一个替代。
使用该注解时,要使用MockitoAnnotations.initMocks 方法,让注解生效, 比如放在@Before方法中初始化。
比较优雅优雅的写法是用MockitoJUnitRunner,它可以自动执行MockitoAnnotations.initMocks 方法。
package com.mockito.learn.service;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Random;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class MocketTestOfAnnotation {
@Mock
private Random random;
@Test
public void test() {
when(random.nextInt()).thenReturn(100);
Assert.assertEquals(100, random.nextInt());
}
}
2.4 参数匹配
如果参数匹配既申明了精确匹配,也声明了模糊匹配;又或者同一个值的精确匹配出现了两次,使用时会匹配符合匹配条件的最新声明的匹配。
package com.mockito.learn.service;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class MockTest {
/**
* 参数匹配1
*/
@Test
public void with_arguments() {
Comparable comparable = mock(Comparable.class);
//预设根据不同的参数返回不同的结果
when(comparable.compareTo("Test")).thenReturn(1);
when(comparable.compareTo("Omg")).thenReturn(2);
assertEquals(1, comparable.compareTo("Test"));
assertEquals(2, comparable.compareTo("Omg"));
//对于没有预设的情况会返回默认值
assertEquals(0, comparable.compareTo("Not stub"));
}
/**
* 参数匹配2
*/
@Test
public void with_arguments_01() {
List list = mock(List.class);
// 精确匹配 0
when(list.get(0)).thenReturn("a");
assertEquals("a", list.get(0));
// 模糊匹配
when(list.get(anyInt())).thenReturn("c");
assertEquals("c", list.get(0));
assertEquals("c", list.get(1));
}
}
@Test
public void all_arguments_provided_by_matchers() {
Comparator comparator = mock(Comparator.class);
comparator.compare("nihao", "hello");
//如果你使用了参数匹配,那么所有的参数都必须通过matchers来匹配
verify(comparator).compare(anyString(), eq("hello"));
//下面的为无效的参数匹配使用
//verify(comparator).compare(anyString(),"hello");
}
@Test
public void argumentMatchersTest() {
//创建mock对象
List<String> mock = mock(List.class);
//argThat(Matches<T> matcher)方法用来应用自定义的规则,可以传入任何实现Matcher接口的实现类。
when(mock.addAll(argThat(new IsListofTwoElements()))).thenReturn(true);
mock.addAll(Arrays.asList("one", "two", "three"));
//IsListofTwoElements用来匹配size为2的List,因为例子传入List为三个元素,所以此时将失败。
verify(mock).addAll(argThat(new IsListofTwoElements()));
}
class IsListofTwoElements implements ArgumentMatcher<List> {
@Override
public boolean matches(List list) {
return list.size() == 2;
}
}
/**
* 捕获参数来进一步断言
* 较复杂的参数匹配器会降低代码的可读性,有些地方使用参数捕获器更加合适。
*/
@Test
public void capturing_args() {
PersonDao personDao = mock(PersonDao.class);
PersonService personService = new PersonService(personDao);
ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
personService.update(1, "jack");
verify(personDao).update(argument.capture());
assertEquals(1, argument.getValue().getId());
assertEquals("jack", argument.getValue().getName());
}
class Person {
private int id;
private String name;
Person(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
}
interface PersonDao {
public void update(Person person);
}
class PersonService {
private PersonDao personDao;
PersonService(PersonDao personDao) {
this.personDao = personDao;
}
public void update(int id, String name) {
personDao.update(new Person(id, name));
}
}
anyInt 只是用来匹配参数的工具之一,目前 mockito 有多种匹配函数,部分如下:
| 函数名 | 匹配类型 |
|---|---|
| any() | 所有对象类型 |
| anyInt() | 基本类型 int、非 null 的 Integer 类型 |
| anyChar() | 基本类型 char、非 null 的 Character 类型 |
| anyShort() | 基本类型 short、非 null 的 Short 类型 |
| anyBoolean() | 基本类型 boolean、非 null 的 Boolean 类型 |
| anyDouble() | 基本类型 double、非 null 的 Double 类型 |
| anyFloat() | 基本类型 float、非 null 的 Float 类型 |
| anyLong() | 基本类型 long、非 null 的 Long 类型 |
| anyByte() | 基本类型 byte、非 null 的 Byte 类型 |
| anyString() | String 类型(不能是 null) |
| anyList() | List 类型(不能是 null) |
| anyMap() | Map<K, V>类型(不能是 null) |
| anyCollection() | Collection类型(不能是 null) |
| anySet() | Set类型(不能是 null) |
| any(Class type) | type类型的对象(不能是 null) |
| isNull() | null |
| notNull() | 非 null |
| isNotNull() | 非 null |
2.5 Mock异常
package com.mockito.learn.service;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
public class MockTest
/**
* 模拟异常
*/
@Test
public void mock_element_test_05() {
LinkedList mockedList = mock(LinkedList.class);
doThrow(new ArithmeticException()).when(mockedList).clear();
// 调用mockedList.clear()方法的时候会抛出异常RuntimeException
assertThrows(ArithmeticException.class, mockedList::clear);
// thenThrow 中可以指定多个异常。在调用时异常依次出现。若调用次数超过异常的数量,再次调用时抛出最后一个异常。
Random mockRandom = mock(Random.class);
when(mockRandom.nextInt()).thenThrow(new ArithmeticException("异常1"), new RuntimeException("异常2"));
assertThrows(ArithmeticException.class, mockRandom::nextInt);
assertThrows(RuntimeException.class, mockRandom::nextInt);
// 对于返回类型是 void 的函数,thenThrow 是无效的,要使用 doThrow。
ExampleService exampleService = mock(ExampleService.class);
// 这种写法可以达到效果
doThrow(new RuntimeException("异常")).when(exampleService).hello();
assertThrows(RuntimeException.class, exampleService::hello);
}
}
2.6 使用spy 方法
Spy 是 Mockito 框架中用于创建部分模拟对象的工具。与完全模拟对象(mock)不同,spy 对象可以部分模拟真实对象的行为,同时保留真实对象的部分方法实现。
spy和mock的异同
相同点
spy和mock生成的对象不受spring管理
不同点
1.默认行为不同
对于未指定mock的方法,spy默认会调用真实的方法,有返回值的返回真实的返回值,而mock默认不执行,有返回值的,默认返回null
2.使用方式不同
Spy中用when...thenReturn私有方法总是被执行,预期是私有方法不应该执行,因为很有可能私有方法就会依赖真实的环境。
- Spy中用doReturn..when才会不执行真实的方法。
- mock中用 when...thenReturn 私有方法不会执行。
3.代码统计覆盖率不同
- @spy使用的真实的对象实例,调用的都是真实的方法,所以通过这种方式进行测试,在进行sonar覆盖率统计时统计出来是有覆盖率;
- @mock出来的对象可能已经发生了变化,调用的方法都不是真实的,在进行sonar覆盖率统计时统计出来的Calculator类覆盖率为0.00%。
Spy 的工作原理
Spy 对象实际上是真实对象的包装。当调用 spy 对象的方法时,Mockito 会首先检查该方法是否被打桩(stubbed)。如果该方法被打桩,则会使用打桩的实现;否则,则会调用真实的方法实现。
Spy 的优点
- 可以部分模拟真实对象的行为,同时保留真实对象的部分方法实现。
- 可以跟踪 spy 对象上方法的调用情况,并进行验证。
- 可以使用 doReturn、doThrow 等方法来打桩 spy 对象的方法。
Spy 的缺点
-
比完全模拟对象更复杂。
-
可能会导致测试代码更难以理解和维护。
package com.mockito.learn.service;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
public class SpyElementTest {
@Test
void spy_on_real_objects() {
List list = new LinkedList();
List spy = spy(list);
//下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
//when(spy.get(0)).thenReturn(3);
//使用doReturn-when可以避免when-thenReturn调用真实对象api
doReturn(999).when(spy).get(999);
//预设size()期望值
when(spy.size()).thenReturn(100);
//调用真实对象的api
spy.add(1);
spy.add(2);
assertEquals(100, spy.size());
assertEquals(1, spy.get(0));
assertEquals(2, spy.get(1));
verify(spy).add(1);
verify(spy).add(2);
assertEquals(999, spy.get(999));
// 对于未指定mock的方法,spy默认会调用真实的方法
assertThrows(IndexOutOfBoundsException.class, () -> spy.get(2));
}
/**
* 真实的部分mock
*/
@DisplayName("真实的部分mock")
@Test
public void real_partial_mock() {
//通过spy来调用真实的api
List list = spy(new ArrayList());
assertEquals(0, list.size());
A a = mock(A.class);
//通过thenCallRealMethod来调用真实的api
when(a.doSomething(anyInt())).thenCallRealMethod();
assertEquals(999, a.doSomething(999));
}
class A {
public int doSomething(int i) {
return i;
}
}
/**
* 重置mock
*/
@DisplayName("重置mock")
@Test
public void reset_mock() {
List list = mock(List.class);
when(list.size()).thenReturn(10);
list.add(1); //Mock for List, hashCode: 400064818
System.out.println(list);
assertEquals(10, list.size());
//重置mock,清除所有的互动和预设
reset(list);
assertEquals(0, list.size());
}
/**
* 验证确切的调用次数
*/
@Test
public void verifying_number_of_invocations() {
List list = mock(List.class);
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(3);
list.add(3);
System.out.println(list); // Mock for List, hashCode: 507819576
//验证是否被调用一次,等效于下面的times(1)
verify(list).add(1);
verify(list, times(1)).add(1);
//验证是否被调用2次
verify(list, times(2)).add(2);
//验证是否被调用3次
verify(list, times(3)).add(3);
//验证是否从未被调用过
verify(list, never()).add(4);
//验证至少调用一次
verify(list, atLeastOnce()).add(1);
//验证至少调用2次
verify(list, atLeast(2)).add(2);
//验证至多调用3次
verify(list, atMost(3)).add(3);
}
//(expected = RuntimeException.class)
@Test
public void consecutive_calls() {
List mockList = mock(List.class);
//模拟连续调用返回期望值,如果分开,则只有最后一个有效
when(mockList.get(0)).thenReturn(0);
when(mockList.get(0)).thenReturn(1);
when(mockList.get(0)).thenReturn(2);
when(mockList.get(1)).thenReturn(0).thenReturn(1).thenThrow(new RuntimeException());
assertEquals(2, mockList.get(0));
assertEquals(2, mockList.get(0));
assertEquals(0, mockList.get(1));
assertEquals(1, mockList.get(1));
//第三次或更多调用都会抛出异常
assertThrows(RuntimeException.class, () -> mockList.get(1));
}
/**
* 验证执行顺序
*/
@Test
public void verification_in_order() {
List list = mock(List.class);
List list2 = mock(List.class);
list.add(1);
list2.add("hello");
list.add(2);
list2.add("world");
//将需要排序的mock对象放入InOrder
InOrder inOrder = inOrder(list, list2);
//下面的代码不能颠倒顺序,验证执行顺序
inOrder.verify(list).add(1);
inOrder.verify(list2).add("hello");
inOrder.verify(list).add(2);
inOrder.verify(list2).add("world");
}
@Test
public void verify_interaction() {
List list = mock(List.class);
List list2 = mock(List.class);
List list3 = mock(List.class);
list.add(1);
verify(list).add(1);
verify(list, never()).add(2);
//验证零互动行为
//verifyZeroInteractions(list2,list3);
}
/**
* 找出冗余的互动(即未被验证到的)
*/
//(expected = NoInteractionsWanted.class)
@Test
public void find_redundant_interaction() {
List list = mock(List.class);
list.add(1);
list.add(2);
verify(list, times(2)).add(anyInt());
//检查是否有未被验证的互动行为,因为add(1)和add(2)都会被上面的anyInt()验证到,所以下面的代码会通过
verifyNoMoreInteractions(list);
List list2 = mock(List.class);
list2.add(1);
list2.add(2);
verify(list2).add(1);
//检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
verifyNoMoreInteractions(list2);
}
// 测试 spy
@Test
public void test_spy() {
ExampleService spyExampleService = spy(new ExampleService());
// 默认会走真实方法
assertEquals(3, spyExampleService.add(1, 2));
// 打桩后,不会走了
when(spyExampleService.add(1, 2)).thenReturn(10);
assertEquals(10, spyExampleService.add(1, 2));
// 但是参数不匹配的调用,依然走真实方法
assertEquals(3, spyExampleService.add(2, 1));
// 通过thenCallRealMethod来使调用真实的方法
when(spyExampleService.add(3, 2)).thenCallRealMethod();
assertEquals(5, spyExampleService.add(3, 2));
}
// 测试 mock
@Test
public void test_mock() {
ExampleService mockExampleService = mock(ExampleService.class);
// 默认返回结果是返回类型int的默认值
assertEquals(0, mockExampleService.add(1, 2));
}
}
class ExampleService {
int add(int a, int b) {
return a + b;
}
}
when(a.doSomething(anyInt())).thenCallRealMethod();
- when(a.doSomething(anyInt())): 这部分代码表示要模拟 a 对象的 doSomething 方法,该方法接受一个 int 类型参数。anyInt() 表示该参数可以是任何 int 值。
- .thenCallRealMethod(): 这部分代码表示当 doSomething 方法被调用时,应调用真实的方法实现。
2.7 使用@Spy 注解
spy 对应注解 @Spy,和 @Mock 是一样用的。
package com.mockito.learn.service;
import org.junit.Test;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
public class SpyElementTestOfAnnotation {
@Spy
private ExampleService spyExampleService;
@Test
public void test_spy() {
MockitoAnnotations.initMocks(this);
assertEquals(3, spyExampleService.add(1, 2));
when(spyExampleService.add(1, 2)).thenReturn(10);
assertEquals(10, spyExampleService.add(1, 2));
}
static class ExampleService {
int add(int a, int b) {
return a + b;
}
}
}
// 写法1
@Spy
private ExampleService spyExampleService;
// 写法2
@Spy
private ExampleService spyExampleService = new ExampleService();
如果没有无参构造函数,必须使用写法2。例子:
public class SpyElementTestOfAnnotation {
@Spy
private ExampleService spyExampleService = new ExampleService(1);
@Test
public void test_spy() {
MockitoAnnotations.initMocks(this);
Assert.assertEquals(3, spyExampleService.add(2));
}
static class ExampleService {
private int a;
public ExampleService(int a) {
this.a = a;
}
int add(int b) {
return a + b;
}
}
}
2.8 测试隔离
根据 JUnit 单测隔离 ,当 Mockito 和 JUnit 配合使用时,也会将非static变量或者非单例隔离开。
比如使用 @Mock 修饰的 mock 对象在不同的单测中会被隔离开。
示例:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class MockitoDemo {
static class ExampleService {
public int add(int a, int b) {
return a+b;
}
}
@Mock
private ExampleService exampleService;
@Test
public void test01() {
System.out.println("---call test01---");
System.out.println("打桩前: " + exampleService.add(1, 2));
when(exampleService.add(1, 2)).thenReturn(100);
System.out.println("打桩后: " + exampleService.add(1, 2));
}
@Test
public void test02() {
System.out.println("---call test02---");
System.out.println("打桩前: " + exampleService.add(1, 2));
when(exampleService.add(1, 2)).thenReturn(100);
System.out.println("打桩后: " + exampleService.add(1, 2));
}
}
将两个单测一起运行,运行结果是:
---call test01---
打桩前: 0
打桩后: 100
---call test02---
打桩前: 0
打桩后: 100
test01 先被执行,打桩前调用add(1, 2)的结果是0,打桩后是 100。然后 test02 被执行,打桩前调用add(1, 2)的结果是0,而非 100,这证明 mock 对象在不同的单测中会被隔离开
2.9 InjectMocks
2.9.1 InjectMock 的使用
InjectMock 是 Mockito 框架中用于注入模拟对象的注解。它可以将模拟对象注入到被测试对象的构造函数或字段中。
package com.mockito.learn.service;
public class CreateWeddingPlanResponse {
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
package com.mockito.learn.service;
import org.springframework.stereotype.Service;
@Service
public class PlannerClient {
public CreateWeddingPlanResponse createWeddingPlan() {
CreateWeddingPlanResponse response = new CreateWeddingPlanResponse();
response.setId(1234L);
return response;
}
}
package com.mockito.learn.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PlannerServiceImpl {
@Autowired
private PlannerClient plannerClient;
public Long createWeddingPlan() {
try {
CreateWeddingPlanResponse response = plannerClient.createWeddingPlan();
return response.getId();
} catch (Exception e) {
return null;
}
}
}
package com.mockito.learn.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class PlannerServiceImplTest {
@Mock
private PlannerClient plannerClient; // 此mock将被注入到plannerService中
@InjectMocks
private PlannerServiceImpl plannerService;
@Test
public void testCreateWeddingPlanWhenClientReturnsUndefinedResponseThenNullIsReturned() throws Exception {
CreateWeddingPlanResponse response = new CreateWeddingPlanResponse();
response.setId(12345L);
when(plannerClient.createWeddingPlan()).thenReturn(response);
final Long actual = plannerService.createWeddingPlan();
System.out.println("actual:" + actual);
}
}
这段代码中,@InjectMock下面声明了一个待测试的对象,若该对象有类型为PlannerClient的成员变量,@Mock定义的mock对象将会被注入到这个待测试的对象中,既PlannerServiceImpl类中的类型为PlannerClient的成员被直接赋值为plannerClient。
该 org.mockito.InjectMocks 注释可以被看作是 Spring 自身依赖注入的等效。Javadoc 指出:
Mockito 将尝试仅通过构造函数注入,setter 注入或属性注入按顺序注入模拟,如下所述。如果以下任何策略失败,则 Mockito 不会报告失败;即,您将必须自己提供依赖项。
3. 一个demo
本例子主要用来测试OrderService类,但是OrderService又依赖于OrderMapper,这时候我们便可以mock出OrderMapper的返回预期值,从而测试OrderService类。
待测试类OrderService
package com.mockito.learn.service;
import com.mockito.learn.entity.Order;
import com.mockito.learn.mapper.OrderMapper;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
OrderMapper orderMapper;
public OrderService(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
public Order getOrder(Integer id) {
return orderMapper.selectOrder(id);
}
}
依赖OrderMapper
package com.mockito.learn.mapper;
import com.mockito.learn.entity.Order;
import org.springframework.stereotype.Repository;
@Repository
public interface OrderMapper {
Order selectOrder(Integer id);
}
测试类
package com.mockito.learn.service;
import com.mockito.learn.entity.Order;
import com.mockito.learn.mapper.OrderMapper;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.math.BigDecimal;
import static org.mockito.Mockito.when;
public class OrderServiceTest {
@Test
void mock_order() {
// mock mockOrderMapper instance
OrderMapper mockOrderMapper = Mockito.mock(OrderMapper.class);
//这里mock的是orderMapper的方法,service间接调用。
when(mockOrderMapper.selectOrder(1)).thenReturn(new Order(0L, "test", BigDecimal.valueOf(1000)));
OrderService orderService = new OrderService(mockOrderMapper);
Order order = orderService.getOrder(1);
System.out.println(order); //Order{id=0, name='test', total=1000}
}
}
4. 总结
- @Mock创建的是全部mock的对象,既在对具体的方法打桩之前,mock对象的所有属性和方法全被置空(0或者null)
- @Spy可以创建部分mock的对象,部分mock对象的所有成员方法都会按照原方法的逻辑执行,直到被打桩返回某个具体的值。
- @Mock和@Spy注解的对象,均可被@InjectMock注入到待处理的对象中。