在今天的学习中,我们将学习如何使用Mockito中的spy、@Spy、@InjectMocks。
Mockito.spy() 方法允许开发者调用对象的常用方法,同时仍然跟踪每一次调用过程。
@Test
public void test0() {
List<String> mock = mock(LinkedList.class);
mock.add("one");
mock.add("two");
List<String> spyList = Mockito.spy(new LinkedList<>());
spyList.add("one");
spyList.add("two");
Mockito.verify(spyList).add("one");
Mockito.verify(spyList).add("two");
//mock方式 实际数组长度为0
assertThat(mock).hasSize(0);
assertThat(spyList).hasSize(2);
}
开发者可以使用@Spy注解替换 spy方法
@Spy
List<String> spyList = new LinkedList<>();
@Test
public void test1() {
spyList.add("one");
Mockito.verify(spyList).add("two");
assertThat(spyList).hasSize(2);
}
启动Mockito注解(例如@Spy、@Mock),开发者需要做相关的配置
在类上使用 @ExtendWith(MockitoExtension.class)
在开始测试之前调用 MockitoAnnotations.openMocks(this); 方法
@BeforeEach
void init(){
MockitoAnnotations.openMocks(this);
}
使用spy方式修改方法行为、跟mock的使用方式基本一致。来看看调用size方法如何改变其返回值
@Test
public void test2() {
List<String> list = new LinkedList<>();
List<String> spyList = Mockito.spy(list);
assertEquals(0, spyList.size());
//调用size方法 返回100
Mockito.doReturn(100).when(spyList).size();
assertThat(spyList).hasSize(100);
}
开发者可以使用doNothing()方法更改spy对象指定的方法行为。意思是:当调用方法时 啥都不干
@Test
public void test04() {
List<String> spy = spy(new LinkedList<>());
//在下标为0处 增加a值 生效
doNothing().when(spy).add(0, "a");
spy.add(0, "a");
//spy.add("hello");
assertThat(spy).hasSize(0);
}
学习Mockito时,难免会遇到Mockito NotAMockException异常。在调用mock、spy方法时,该异常为框架内部的比较有代表性的异常。
在开发时不注意可能会导致此类问题
@Test
void test3(){
List<String> list = new LinkedList<String>();
Mockito.doReturn(100).when(list).size();
}
在上面的示例中,并没有对 list对象进行mock。when方法期望得到一个mock、spy对象作为参数
org.mockito.exceptions.misusing.NotAMockException:
Argument passed to when() is not a mock!
Example of correct stubbing:
doThrow(new RuntimeException()).when(mock).someMethod();
更改后的代码
@Test
void test3(){
// List list = new LinkedList<>();
// Mockito.doReturn(100).when(list).size();
final List<String> spyList = Mockito.spy(new LinkedList<>());
assertThatNoException().isThrownBy(() -> Mockito.doReturn(100).when(spyList).size());
}
Mockito 使用以下三种方式,按照指定的顺序注入mock依赖关系:
基于构造函数的注入-当为类定义了构造函数时,Mockito尝试使用最大的构造函数注入依赖项。
基于Setter方法-当没有定义构造函数时,Mockito尝试使用Setter方法注入依赖项。
基于字段-如果没有可能的构造函数或基于字段的注入,那么mockito会尝试将依赖项注入字段本身。
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.util.Map;
@AllArgsConstructor
@NoArgsConstructor
public class MyDictionary {
private Map<String, String> wordMap;
public String getMeaning(final String word) {
return wordMap.get(word);
}
}
@ExtendWith(MockitoExtension.class)
public class SpyTest {
@Mock
Map<String, String> mockMap;
//使用 InjectMocks 注解自动向 MyDictionary构造函数注入 mockMap
@InjectMocks
MyDictionary dic = new MyDictionary();
@Test
public void test05() {
//dic 里面的map 跟 mockMap是同一个对象
Mockito.when(mockMap.get("aWord")).thenReturn("aMeaning");
assertEquals("aMeaning", dic.getMeaning("aWord"));
}
}
when(employee.greet()).thenReturn("Hello");
doReturn("Hello").when(employee).greet();
上述代码都能实现特定方法的mock功能,并指定返回。但是在一个test unit中并不能同时出现,需注释其中一行代码才能正常运行。那么when、doXXX之间有什么区别?
为了验证它们之间的差异,先定义一个普通的接口。该接口定义两个方法,其中一个有返回值,一个没有。
import java.time.DayOfWeek;
public interface Employee {
String greet();
void work(DayOfWeek day);
}
测试有返回值的方法 - 以下测试用例可以正常运行
@ExtendWith(MockitoExtension.class)
public class EmployeeTest {
@Mock
Employee employee;
@Test
void test01() {
// given
when(employee.greet()).thenReturn("Hello");
//doReturn("Hello").when(employee).greet();
// when
String greeting = employee.greet();
// then
assertThat(greeting).isEqualTo("Hello");
}
}
测试无返回值的方法
@Test
void test02() {
when(employee.work(DayOfWeek.SUNDAY)).thenThrow(new RuntimeException());
// then
assertThrows(RuntimeException.class, () -> employee.work(DayOfWeek.SUNDAY));
}
上述代码将不能进行编译,原因是work方法并没有返回值,因此不能对返回对象进行mock。需要将代码改成doxxx的模式才能正常运行,如下:
@Test
void test02() {
//when(employee.work(DayOfWeek.SUNDAY)).thenThrow(new RuntimeException());
// given
doThrow(new RuntimeException()).when(employee).work(DayOfWeek.SUNDAY);
// then
assertThrows(RuntimeException.class, () -> employee.work(DayOfWeek.SUNDAY));
}
Mockito框架中的BDDMockito类也提供了另外一种替代语法,只需要将when -> given, do -> will, 其它代码不变
有返回值方法
given(employee.greet()).willReturn("Hello");
无返回值方法
@Test
void test04() {
willThrow(new RuntimeException()).given(employee).work(DayOfWeek.SUNDAY);
assertThrows(RuntimeException.class, () -> employee.work(DayOfWeek.SUNDAY));
}
关于java中的回调方法,来看看Mockito框架如何进行测试。
回调定义: 回调函数作为参数传递给方法的一段代码,该方法预期在给定时间回调(执行参数)。更为经典的是,可能会在稍后的时间发生,有点类似异步回调。
先按顺序定义以下基础类
public interface Service {
void doAction(String request, Callback<String> callback);
}
public interface Callback<T> {
String reply(T response);
}
public class ActionHandler {
private Service service;
public ActionHandler(Service service) {
this.service = service;
}
public void doAction() {
service.doAction("our-request", new Callback<String>() {
@Override
public String reply(String response) {
return handleResponse(response);
}
});
}
private String handleResponse(String response) {
return response + " world";
}
}
public class CallbackTest {
@Mock
private Service service;
//对参数捕获
@Captor
private ArgumentCaptor<Callback<String>> callbackCaptor;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
public void test01() {
ActionHandler handler = new ActionHandler(service);
handler.doAction();
verify(service).doAction(anyString(), callbackCaptor.capture());
Callback<String> callback = callbackCaptor.getValue();
String response = callback.reply("hello");
assertEquals(
"hello world",
response, "yes or no");
}
}
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>6.0.2version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.12.0version>
dependency>
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-junit-jupiterartifactId>
<version>4.9.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>31.1-jreversion>
dependency>
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-coreartifactId>
<version>4.9.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.assertjgroupId>
<artifactId>assertj-coreartifactId>
<version>3.23.1version>
<scope>testscope>
dependency>
dependencies>