一文讓你快速上手 Mockito 單元測試框架_台中搬家

1{icon} {views}

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

前言

在計算機編程中,單元測試是一種軟件測試方法,通過該方法可以測試源代碼的各個單元功能是否適合使用。為代碼編寫單元測試有很多好處,包括可以及早的發現代碼錯誤,促進更改,簡化集成,方便代碼重構以及許多其它功能。使用 Java 語言的朋友應該用過或者聽過 Junit 就是用來做單元測試的,那麼為什麼我們還需要 Mockito 測試框架呢?想象一下這樣的一個常見的場景,當前要測試的類依賴於其它一些類對象時,如果用 Junit 來進行單元測試的話,我們就必須手動創建出這些依賴的對象,這其實是個比較麻煩的工作,此時就可以使用 Mockito 測試框架來模擬那些依賴的類,這些被模擬的對象在測試中充當真實對象的虛擬對象或克隆對象,而且 Mockito 同時也提供了方便的測試行為驗證。這樣就可以讓我們更多地去關注當前測試類的邏輯,而不是它所依賴的對象。

生成 Mock 對象方式

要使用 Mockito,首先需要在我們的項目中引入 Mockito 測試框架依賴,基於 Maven 構建的項目引入如下依賴即可:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.3.3</version>
    <scope>test</scope>
</dependency>

如果是基於 Gradle 構建的項目,則引入如下依賴:

testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'

使用 Mockito 通常有兩種常見的方式來創建 Mock 對象。

1、使用 Mockito.mock(clazz) 方式

通過 Mockito 類的靜態方法 mock 來創建 Mock 對象,例如以下創建了一個 List 類型的 Mock 對象:

List<String> mockList = Mockito.mock(ArrayList.class);

由於 mock 方法是一個靜態方法,所以通常會寫成靜態導入方法的方式,即 List mockList = mock(ArrayList.class)。

2、使用 @Mock 註解方式

第二種方式就是使用 @Mock 註解方式來創建 Mock 對象,使用該方式創需要注意的是要在運行測試方法前使用 MockitoAnnotations.initMocks(this) 或者單元測試類上加上 @ExtendWith(MockitoExtension.class) 註解,如下所示代碼創建了一個 List 類型的 Mock 對象(PS: @BeforeEach 是 Junit 5 的註解,功能類似於 Junit 4 的 @Before 註解。):

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
//@ExtendWith(MockitoExtension.class)
public class MockitoTest {

  @Mock
  private List<String> mockList;

  @BeforeEach
  public void beforeEach() {
    MockitoAnnotations.initMocks(this);
  }
}

驗證性測試

Mockito 測試框架中提供了 Mockito.verify 靜態方法讓我們可以方便的進行驗證性測試,比如方法調用驗證、方法調用次數驗證、方法調用順序驗證等,下面看看具體的代碼。

驗證方法單次調用

驗證方法單次調用的話直接 verify 方法后加上待驗證調用方法即可,以下代碼的功能就是驗證 mockList 對象的 size 方法被調用一次。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_SimpleInvocationOnMock() {
    mockList.size();
    verify(mockList).size();
  }
}
驗證方法調用指定次數

除了驗證單次調用,我們有時候還需要驗證一些方法被調用多次或者指定的次數,那麼此時就可以使用 verify + times 方法來驗證方法調用指定次數,同時還可以結合 atLeast + atMost 方法來提供調用次數範圍,同時還有 never 等方法驗證不被調用等。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_NumberOfInteractionsWithMock() {
    mockList.size();
    mockList.size();

    verify(mockList, times(2)).size();
    verify(mockList, atLeast(1)).size();
    verify(mockList, atMost(10)).size();
  }
}
驗證方法調用順序

同時還可以使用 inOrder 方法來驗證方法的調用順序,下面示例驗證 mockList 對象的 size、add 和 clear 方法的調用順序。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_OrderedInvocationsOnMock() {
    mockList.size();
    mockList.add("add a parameter");
    mockList.clear();

    InOrder inOrder = inOrder(mockList);

    inOrder.verify(mockList).size();
    inOrder.verify(mockList).add("add a parameter");
    inOrder.verify(mockList).clear();
  }
}

以上只是列舉了一些簡單的驗證性測試,還有驗證測試方法調用超時以及更多的驗證測試可以通過相關官方文檔探索學習。

驗證方法異常

異常測試我們需要使用 Mockito 框架提供的一些調用行為定義,Mockito 提供了 when(…).thenXXX(…) 來讓我們定義方法調用行為,以下代碼定義了當調用 mockMap 的 get 方法無論傳入任何參數都會拋出一個空指針 NullPointerException 異常,然後通過 Assertions.assertThrows 來驗證調用結果。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoExceptionTest {

  @Mock
  public Map<String, Integer> mockMap;

  @Test
  public void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
    when(mockMap.get(anyString())).thenThrow(NullPointerException.class);

    assertThrows(NullPointerException.class, () -> mockMap.get("mghio"));
  }
}

同時 when(…).thenXXX(…) 不僅可以定義方法調用拋出異常,還可以定義調用方法后的返回結果,比如 when(mockMap.get(“mghio”)).thenReturn(21); 定義了當我們調用 mockMap 的 get 方法並傳入參數 mghio 時的返回結果是 21。這裡有一點需要注意,使用以上這種方式定義的 mock 對象測試實際並不會影響到對象的內部狀態,如下圖所示:

雖然我們已經在 mockList 對象上調用了 add 方法,但是實際上 mockList 集合中並沒有加入 mghio,這時候如果需要對 mock 對象有影響,那麼需要使用 spy 方式來生成 mock 對象。

public class MockitoTest {

  private List<String> mockList = spy(ArrayList.class);

  @Test
  public void add_spyMockList_thenAffect() {
    mockList.add("mghio");

    assertEquals(0, mockList.size());
  }
}

斷點后可以發現當使用 spy 方法創建出來的 mock 對象調用 add 方法后,mghio 被成功的加入到 mockList 集合當中。

與 Spring 框架集成

Mockito 框架提供了 @MockBean 註解用來將 mock 對象注入到 Spring 容器中,該對象會替換容器中任何現有的相同類型的 bean,該註解在需要模擬特定bean(例如外部服務)的測試場景中很有用。如果使用的是 Spring Boot 2.0+ 並且當前容器中已有相同類型的 bean 的時候,需要設置 spring.main.allow-bean-definition-overriding 為 true(默認為 false)允許 bean 定義覆蓋。下面假設要測試通過用戶編碼查詢用戶的信息,有一個數據庫操作層的 UserRepository,也就是我們等下要 mock 的對象,定義如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Repository
public interface UserRepository {

  User findUserById(Long id);

}

還有用戶操作的相關服務 UserService 類,其定義如下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Service
public class UserService {

  private UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User findUserById(Long id) {
    return userRepository.findUserById(id);
  }
}

在測試類中使用 @MockBean 來標註 UserRepository 屬性表示這個類型的 bean 使用的是 mock 對象,使用 @Autowired 標註表示 UserService 屬性使用的是 Spring 容器中的對象,然後使用 @SpringBootTest 啟用 Spring 環境即可。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@SpringBootTest
public class UserServiceUnitTest {

  @Autowired
  private UserService userService;

  @MockBean
  private UserRepository userRepository;

  @Test
  public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
    User expectedUser = new User(9527L, "mghio", "18288888880");
    when(userRepository.findUserById(9527L)).thenReturn(expectedUser);
    User actualUser = userService.findUserById(9527L);
    assertEquals(expectedUser, actualUser);
  }
}

Mockito 框架的工作原理

通過以上介紹可以發現, Mockito 非常容易使用並且可以方便的驗證一些方法的行為,相信你已經看出來了,使用的步驟是先創建一個需要 mock 的對象 Target ,該對象如下:

public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

然後我們直接使用 Mockito.mock 方法和 when(…).thenReturn(…) 來生成 mock 對象並指定方法調用時的行為,代碼如下:

@Test
public void test_foo() {
  String expectedResult = "Mocked mghio";
  when(mockTarget.foo("mghio")).thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

仔細觀察以上 when(mockTarget.foo(“mghio”)).thenReturn(expectedResult) 這行代碼,首次使用我也覺得很奇怪,when 方法的入參竟然是方法的返回值 mockTarget.foo(“mghio”),覺得正確的代碼應該是這樣 when(mockTarget).foo(“mghio”),但是這個寫法實際上無法進行編譯。既然 Target.foo 方法的返回值是 String 類型,那是不是可以使用如下方式呢?

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

Mockito.when("Hello, I am mghio").thenReturn("Mocked mghio");

結果是編譯通過,但是在運行時報錯:

從錯誤提示可以看出,when 方法需要一個方法調用的參數,實際上它只需要 more 對象方法調用在 when 方法之前就行,我們看看下面這個測試代碼:

@Test
public void test_mockitoWhenMethod() {
  String expectedResult = "Mocked mghio";
  mockTarget.foo("mghio");
  when("Hello, I am mghio").thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

以上代碼可以正常測試通過,結果如下:

為什麼這樣就可以正常測試通過?是因為當我們調用 mock 對象的 foo 方法時,Mockito 會攔截方法的調用然後將方法調用的詳細信息保存到 mock 對象的上下文中,當調用到 Mockito.when 方法時,實際上是從該上下文中獲取最後一個註冊的方法調用,然後把 thenReturn 的參數作為其返回值保存,然後當我們的再次調用 mock 對象的該方法時,之前已經記錄的方法行為將被再次回放,該方法觸發攔截器重新調用並且返回我們在 thenReturn 方法指定的返回值。以下是 Mockito.when 方法的源碼:

該方法裏面直接使用了 MockitoCore.when 方法,繼續跟進,該方法源碼如下:

仔細觀察可以發現,在源碼中並沒有用到參數 methodCall,而是從 MockingProgress 實例中獲取 OngoingStubbing 對象,這個 OngoingStubbing 對象就是前文所提到的上下文對象。個人感覺是 Mockito 為了提供簡潔易用的 API 然後才製造了 when 方法調用的這種“幻象”,簡而言之,Mockito 框架通過方法攔截在上下文中存儲和檢索方法調用詳細信息來工作的。

如何實現一個微型的 Mock 框架

知道了 Mockito 的運行原理之後,接下來看看要如何自己去實現一個類似功能的 mock 框架出來,看到方法攔截這裏我相信你已經知道了,其實這就是 AOP 啊,但是通過閱讀其源碼發現 Mockito 其實並沒有使用我們熟悉的 Spring AOP 或者 AspectJ 做的方法攔截,而是通過運行時增強庫 Byte Buddy 和反射工具庫 Objenesis 生成和初始化 mock 對象的。
現在,通過以上分析和源碼閱讀可以定義出一個簡單版本的 mock 框架了,將自定義的 mock 框架命名為 imock。這裡有一點需要注意的是,Mockito 有一個好處是,它不需要進行初始化,可以直接通過其提供的靜態方法來立即使用它。在這裏我們也使用相同名稱的靜態方法,通過 Mockito 源碼:

很容易看出 Mockito 類最終都是委託給 MockitoCore 去實現的功能,而其只提供了一些面向使用者易用的靜態方法,在這裏我們也定義一個這樣的代理對象 IMockCore,這個類中需要一個創建 mock 對象的方法 mock 和一個給方法設定返回值的 thenReturn 方法,同時該類中持有一個方法調用詳情 InvocationDetail 集合列表,這個類是用來記錄方法調用詳細信息的,然後 when 方法僅返回列表中的最後一個 InvocationDetail,這裏列表可以直接使用 Java 中常用的 ArrayList 即可,這裏的 ArrayList 集合列表就實現了 Mockito 中的 OngoingStubbing 的功能。
根據方法的三要素方法名、方法參數和方法返回值很容易就可以寫出 InvocationDetail 類的代碼,為了對方法在不同類有同名的情況區分,還需要加上類全稱字段和重寫該類的 equals 和 hashCode 方法(判斷是否在調用方法集合列表時需要根據該方法判斷),代碼如下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class InvocationDetail<T> {

  private String attachedClassName;

  private String methodName;

  private Object[] arguments;

  private T result;

  public InvocationDetail(String attachedClassName, String methodName, Object[] arguments) {
    this.attachedClassName = attachedClassName;
    this.methodName = methodName;
    this.arguments = arguments;
  }

  public void thenReturn(T t) {
    this.result = t;
  }

  public T getResult() {
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    InvocationDetail<?> behaviour = (InvocationDetail<?>) o;
    return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
        Objects.equals(methodName, behaviour.methodName) &&
        Arrays.equals(arguments, behaviour.arguments);
  }

  @Override
  public int hashCode() {
    int result = Objects.hash(attachedClassName, methodName);
    result = 31 * result + Arrays.hashCode(arguments);
    return result;
  }
}

接下來就是如何去創建我們的 mock 對象了,在這裏我們也使用 Byte Buddy 和 Objenesis 庫來創建 mock 對象,IMockCreator 接口定義如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public interface IMockCreator {

  <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList);

}

實現類 ByteBuddyIMockCreator 使用 Byte Buddy 庫在運行時動態生成 mock 類對象代碼然後使用 Objenesis 去實例化該對象。代碼如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class ByteBuddyIMockCreator implements IMockCreator {

  private final ObjenesisStd objenesisStd = new ObjenesisStd();

  @Override
  public <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList) {
    ByteBuddy byteBuddy = new ByteBuddy();

    Class<? extends T> classWithInterceptor = byteBuddy.subclass(mockTargetClass)
        .method(ElementMatchers.any())
        .intercept(MethodDelegation.to(InterceptorDelegate.class))
        .defineField("interceptor", IMockInterceptor.class, Modifier.PRIVATE)
        .implement(IMockIntercepable.class)
        .intercept(FieldAccessor.ofBeanProperty())
        .make()
        .load(getClass().getClassLoader(), Default.WRAPPER).getLoaded();

    T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
    ((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(behaviorList));

    return mockTargetInstance;
  }
}

基於以上分析我們可以很容易寫出創建 mock 對象的 IMockCore 類的代碼如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockCore {

  private final List<InvocationDetail> invocationDetailList = new ArrayList<>(8);

  private final IMockCreator mockCreator = new ByteBuddyIMockCreator();

  public <T> T mock(Class<T> mockTargetClass) {
    T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
    return result;
  }

  @SuppressWarnings("unchecked")
  public <T> InvocationDetail<T> when(T methodCall) {
    int currentSize = invocationDetailList.size();
    return (InvocationDetail<T>) invocationDetailList.get(currentSize - 1);
  }
}

提供給使用者的類 IMock 只是對 IMockCore 進行的簡單調用而已,代碼如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMock {

  private static final IMockCore IMOCK_CORE = new IMockCore();

  public static <T> T mock(Class<T> clazz) {
    return IMOCK_CORE.mock(clazz);
  }

  public static <T> InvocationDetail when(T methodCall) {
    return IMOCK_CORE.when(methodCall);
  }
}

通過以上步驟,我們就已經實現了一個微型的 mock 框架了,下面來個實際例子測試一下,首先創建一個 Target 對象:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

然後編寫其對應的測試類 IMockTest 類如下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockTest {

  @Test
  public void test_foo_method() {
    String exceptedResult = "Mocked mghio";
    Target mockTarget = IMock.mock(Target.class);

    IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);

    String actualResult = mockTarget.foo("mghio");

    assertEquals(exceptedResult, actualResult);
  }

}

以上測試的可以正常運行,達到了和 Mockito 測試框架一樣的效果,運行結果如下:

上面只是列出了一些關鍵類的源碼,自定義 IMock 框架的所有代碼已上傳至 Github 倉庫 imock,感興趣的朋友可以去看看。

總結

本文只是介紹了 Mockito 的一些使用方法,這隻是該框架提供的最基礎功能,更多高級的用法可以去官網閱讀相關的文檔,然後介紹了框架中 when(…).thenReturn(…) 定義行為方法的實現方式並按照其源碼思路實現了一個相同功能的簡易版的 imock 。雖然進行單元測試有很多優點,但是也不可盲目的進行單元測試,在大部分情況下只要做好對項目中邏輯比較複雜、不容易理解的核心業務模塊以及項目中公共依賴的模塊的單元測試就可以了。

參考文章

Mockito
Objenesis
Byte Buddy

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司