Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。
Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。
————>
引入mockito
新建一gradle
项目,增加以下依赖项目build.gradle
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
testImplementation 'org.mockito:mockito-core:5.3.1'
}
mock对象
通过Mockito.mock()
方法可以创建一个模拟对象,我们可以控制这个对象的行为表现。
class MockitoProgrammaticallyUnitTest {
@Test
@DisplayName("编程方式使用mock")
void test1() {
List<String> mockedList = Mockito.mock(List.class); // 创建一个List的模拟对象
mockedList.add("one"); // 实际上什么都没做
Mockito.verify(mockedList).add("one"); // 验证模拟对象的add方法被调用了
Assertions.assertEquals(0, mockedList.size()); // 因为上面的add并没有真实的添加数据,所以此处大小应该是0
Mockito.when(mockedList.size()).thenReturn(100); // 假定 模拟对象的size方法被调用,那么该方法返回100
Assertions.assertEquals(100, mockedList.size()); // 当 调用模拟对象的size方法时 期望返回的值为100
}
}
被模拟的对象是一个类型,不是一个具体的实例,无需创建具体实例,就可以创建模拟类。也就是说模拟类没有具体实现,行为需要我们提前指定。 那么我们是否可以对具体的实例模拟操作呢?答案是可以的。
对象探针 spy
通过Mockito.spy()
方法,我们可以把一个实例转换成一个探针对象。这样我就能控制这个对象的行为表现了,和mock对象不同的是探针对像的默认行为是真实行为。
class MockitoProgrammaticallyUnitTest {
@Test
@DisplayName("编程方式使用spy")
void test2(){
List<String> spyList = Mockito.spy(new ArrayList<>()); // 把一个ArrayList实例转换成探针对象
spyList.add("one"); // 这里会发生真实调用
spyList.add("two");
Mockito.verify(spyList).add("one"); // 验证探针对象的调用情况
Mockito.verify(spyList).add("two");
Assertions.assertEquals(2,spyList.size()); // 因为发生了真实调用 所以大小为2
Mockito.doReturn(100).when(spyList).size(); // 指定行为 当调用size时返回100
Assertions.assertEquals(100,spyList.size()); // 此时 size的行为就被改变成了指定的行为了,不是默认的真实行为了。
}
}
参数抓取captor
上面的例子我们演示了,模拟对象行为、验证模拟对象方法时否被调用,那么我们如何验证方法被调用时所传递的参数呢?那就需要用到ArgumentCaptor
这个工具了。
class MockitoProgrammaticallyUnitTest {
@Test
@DisplayName("编程方式使用captor")
void test3(){
List<String> mockedList = Mockito.mock(List.class); // 创建一个List模拟对象
mockedList.add("one"); //调用add方法 传递一个 one 作为参数
ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class); // 创建一个参数抓取对象
Mockito.verify(mockedList).add(arg.capture()); //验证模拟对象add方法是否被调用,同时设置上参数抓取对象
Assertions.assertEquals("one",arg.getValue()); // 断言抓取到的参数是 one
}
}
注解方式使用mock
上面的例子使用的是编程方式创建模拟对象。还有一种通过注解方式创建模拟对象当方法,使用起来非常方便,下面我用注解的方式该写一下上面的例子:
class MockitoAnnotationsUnitTest {
@Mock // 使用@Mock 代替 Mockito.mock()
List<String> mockedList;
@Spy // 使用@Spy 代替 Mockito.spy()
List<String> spyList = Mockito.spy(new ArrayList<>());
@Captor // 使用@Captor代替 ArguentCaptor.forClass()
ArgumentCaptor<String> arg;
//下面的7行代码要注意,否则注解是不起作用的
private AutoCloseable closeable;
@BeforeEach
public void init() {
closeable= MockitoAnnotations.openMocks(this);
}
@AfterEach
void releaseMocks() throws Exception {
closeable.close();
}
@Test
@DisplayName("注解方式使用mock")
void test1() {
mockedList.add("one");
Mockito.verify(mockedList).add("one");
Assertions.assertEquals(0, mockedList.size());
Mockito.when(mockedList.size()).thenReturn(100);
Assertions.assertEquals(100, mockedList.size());
}
@Test
@DisplayName("注解方式使用spy")
void test2() {
spyList.add("one");
spyList.add("two");
Mockito.verify(spyList).add("one");
Mockito.verify(spyList).add("two");
Assertions.assertEquals(2, spyList.size());
Mockito.doReturn(100).when(spyList).size();
Assertions.assertEquals(100, spyList.size());
}
@Test
@DisplayName("注解方式使用captor")
void test3() {
List<String> mockedList = Mockito.mock(List.class);
mockedList.add("one");
Mockito.verify(mockedList).add(arg.capture());
Assertions.assertEquals("one", arg.getValue());
}
}
模拟静态方法
上面我们对实例方法的行为进行了定制,下面演示一下定制静态方法的行为应该如何操作。
// 先创建一个有静态方法的类
public final class MyUtils {
public static String sayHello(){
return "hello";
}
}
@Test
@DisplayName("mock静态方法")
void testStatic(){
MockedStatic<MyUtils> mockMyUtil = Mockito.mockStatic(MyUtils.class); // 创建一个静态mock
mockMyUtil.when(MyUtils::sayHello).thenReturn("world"); // 指定静态方法的行为
Assertions.assertEquals("world",MyUtils.sayHello()); //断言静态方法的行为
mockMyUtil.close(); // 记得关闭!!
}
注入mock对象
通过@InjectMocks
注解我们可以很方便的把一个实例的属性转换成模拟对象。
// 声明要测试的对象
public class MyDictionary {
private Map<String,String> wordMap; //把它转成模拟对象
public MyDictionary(){
wordMap=new HashMap<>();
}
public void add(String word,String meaning){
wordMap.put(word,meaning);
}
public String getMeaning(String world){
return wordMap.get(world);
}
}
class MockitoAnnotationsUnitTest {
@Mock
Map<String, String> wordMap; //声明一个模拟对象,注意属性名称要一致 或者使用 @Mock(name="wordMap")指定名称
@InjectMocks // 把模拟对象注入到实例中
MyDictionary dic = new MyDictionary();
private AutoCloseable closeable;
@BeforeEach
public void init() {
closeable= MockitoAnnotations.openMocks(this);
}
@AfterEach
void releaseMocks() throws Exception {
closeable.close();
}
@Test
@DisplayName("注入mock")
void test4() {
Mockito.when(wordMap.get("aWord")).thenReturn("aMeaning"); //指定模拟对象的行为
Assertions.assertEquals("aMeaning",dic.getMeaning("aWord")); // 断言被测试对象的行为
}
}
思考
有一个UrlFetcher
类,我们如何对这个类进行单元测试?
public class UrlFetcher {
private URL url;
public UrlFetcher(URL url) throws IOException {
this.url = url;
}
public boolean isUrlAvailable() throws IOException {
return getResponseCode() == HttpURLConnection.HTTP_OK;
}
private int getResponseCode() throws IOException {
HttpURLConnection con = (HttpURLConnection) this.url.openConnection();
return con.getResponseCode();
}
}