Mockito入门指引

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();
  }
}