Java Unit Test — Mockito

Vivi Wang
16 min readJun 13, 2021

--

在軟體工程上測試是一項重要的工作,軟體測試基本流程可分為單元測試 (Unit Test)整合測試 (Integration Test)功能測試 (Functional Test) 或端對端測試 (End to End Test),此篇主要介紹 Java 單元測試工具 — Mockito。

前言

什麼是單元測試?

Unit testing is a way to test units — the smallest components of your software, the smallest piece of code.

單元測試可以說是最貼近程式開發者的測試。
其針對程式撰寫時的最小單位進行正確性驗證的測試,一個單元(Unit)可以是單支程式、方法或過程,在物件導向的程式設計中,最小的單元就是方法。

單元測試很重要的特性是,它應該獨立於其他案例,測試時如果依賴其他外部系統(例如實際串接外部 API、連線 DB 的依賴),這便不是一個好的單元測試。

關於單元、整合、功能測試

這三種測試時的角色簡單歸納為:
- Unit Test: 主要由程式開發人員自行撰寫測試。
- Integration Test: 若公司有專職的測試人員(例如 QA 團隊)通常會由其擔任整合測試角色,沒有的話,一般來說會是請該模組開發人員負責。
- End to End Test: 使用者角度出發的測試,可以透過人工對已經完整部屬的網站進行測試,為人工測式的主要範圍。

Source: Amazon Alexa: Unit Testing: Creating Functional Alexa Skills

對於這三種測試的較詳細定義,這邊就不詳述,可以參考保哥這邊的介紹:
https://blog.miniasp.com/post/2019/02/18/Unit-testing-Integration-testing-e2e-testing

主題

Mock Test 是什麼?

在 mock testing 中,程式原先依賴的對象都可以透過 mock 方式建立一個假的對象去模擬真實行為。

上面介紹單元測試時,有提到不該依賴其他外部系統,Mock Test 是一種單元測試方法,讓我們能更專注於要測試的程式中,避免測試一個方法卻要建構整個 bean 的 dependency chain 的情況發生。

Mock Test 有什麼好處?

假如今天要測試 ClassA 的 methodA 方法,這個方法中用到 classB 與 repositoryA 的方法。

public class ClassA {

@Autowired ClassB classB;
@Autowired RepositoryA repositoryA;

public Response methodA() {
// classB do something...
// repositoryA do something...
return response;
}
}

假如今天沒有使用 mock 的話,測試會需要考慮這兩個依賴的回傳值是不是每次都能符合預期,會發生今天測試有通過,結果明天卻失敗的問題,因為你的返回結果會直接的受外部服務影響,變得無法預期。

使用前的依賴關係

A依賴B,但B本身可能也有依賴C跟D…

使用後的依賴關係

可以專注於ClassA的測試

Mockito 簡介

Mockito 是一種 Java mock 框架,他主要就是用來做 mock 測試的,他可以模擬任何 Spring 管理的 bean、模擬方法的返回值、模擬拋出異常等。

再次複習一下:單元測試的思路是想「在不涉及依賴關係的情況下測試程式碼」,這種測試可以讓你無視程式碼的依賴關係去測試程式碼的有效性。

Mockito 強項在於,可以單獨測試 Service 類程式。像是可以在單元測試中模擬 service 返回值,而不會真正去調用該 service,這就是上面提到的 mock 測試精神,亦即通過模擬一個假的 service 對象,來快速的測試當前要測試的程式。

Spring Boot 套用 Mockito

與 JUnit 相比,Mockito 最適合用來測試包含依賴注入環節的程式。

不管使用 maven 或是 gradle 管理,只要引入spring-boot-starter-test的 2.2.0 版本以上就可以囉!因為在此版本以後都會涵蓋 Junit 5、Hamcrest 及 Mockito 的 Libraries 供測試使用,不用再引用一大堆 dependency 真的簡潔許多,以下為pom.xml範例:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

範例可以參考看看這篇:https://frontbackend.com/spring-boot/spring-boot-2-junit-5-mockito

這邊就不贅述,網路上有很多用 Spring Boot + Mockito 寫單元測試的資源,以下想分享一些常用的方法跟參數。

Mockito 常用註解和方法

先來介紹宣告 mock object 的幾種方式以及有什麼區別

  • Mockito.mock()

使用此方式宣告會創建一個 class 或 interface 的模擬對象,可以模擬方法的返回值,也有利於驗證這個對象的方法是否被調用,舉例:

@Test 
public void testMock() {
UserRepository repository = Mockito.mock(UserRepository.class);
Mockito.when(repository.count()).thenReturn(111L);
long userCount = repository.count();
Assert.assertEquals(111L, userCount);
Mockito.verify(repository).count();
}

這邊可以看到一開始先將 UserRepository 用 Mockito.mock() 方式建立一個 mock object,便可以使用 Mockito.when(…).thenReturn(…)的方式去模擬呼叫方法後的回傳值。

  • @Mock

這個方式其實和上面 Mockito.mock() 效果一樣,寫起來較簡潔,但需要讓 Mockito annotation 去使用此 annotation,有兩種方式:

方法一:在測試的 class 上加上 @RunWith(MockitoJUnitRunner.class) 或是 Junit5 後可改用 @ExtendWith(MockitoExtension.class)

@ExtendWith(MockitoExtension.class)
public class testMockAnnotation {
@Mock UserRepository repository;
@Test
public void testMock({
Mockito.when(repository.count()).thenReturn(123L);
long userCount = repository.count();
Assert.assertEquals(123L, userCount);
Mockito.verify(repository).count();
}
}

方法二:加上 MockitoAnnotations.initMocks()

// 這邊建議寫在測試執行前
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
  • @MockBean

如果測試需要依賴於 Spring Boot 容器,就需要使用Spring Boot中的@MockBean。使用這個方式可以將真正的 Java Bean 透過模擬的方式產生在 IOC Container。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleControllerIT {

@Autowired
private RestTemplate testRestTemplate;

@MockBean
UserRepository userRepository;

@Test
public void retrieveProductItem() throws Exception{
when(userRepository.findAll()).thenReturn(
Arrays.asList(new User(1,"Vivi","09xxxxxxxx"),
new User(2,"Ming","09xxxxxxxx")));
String response = this.testRestTemplate.getForObject("/users", String.class);
JSONAssert.assertEquals("[{id:1},{id:2}]",response,false);
}
}

上面範例中,程式會呼叫 /users 端口取 DB 資料,但我們並不想要實際連到 DB。
這個 UserRepository 是註冊在 Spring IOC Container 中,透過 @MockBean 的方式讓 Mockito Framework 去 mock 出一個在容器中的 UserRepository,在這種情況下若使用 @Mock 的方式會無法運行測試。

  • @InjectMock

當你想要測試的 service 有太多的 dependency classes,可以使用 @InjectMock 標註,但要注意以下兩點:依賴的 classes 必須要被標註為 @ Mock 以及要在測試類別上標記 @RunWith(MockitoJUnitRunner.class) 或是 Junit5 後可改用 @ExtendWith(MockitoExtension.class)

例如:
要測試的為 ExampleService,可以看到它有兩個 dependency classes

public class ExampleService {

private UserService userService;
private AddressService addressService;

public RegistrationService(UserService userService, AddressService addressService) {
this.userService = userService;
this.addressService = addressService;
}

public String doExample(){
String resultA = userService.methodA();
// ... do something
String resultB = addressService.methodB();
// ... do something
}
}

此時需要去 mock 這兩個 denpendency,並且注入到 ExampleService 中

@ExtendWith(MockitoExtension.class)
public class ExampleServiceTest {
@Mock private UserService userService;
@Mock private AddressService addressService;
@InjectMocks private ExampleService exampleService;
@Test
public void doMockTest() {
when(userService.methodA()).thenReturn("Mock A");
when(addressService.methodB()).thenReturn("Mock B");
String response = registrationService.doExample();
// ... do something like AssertsEqual
}
}

參考網路上的小結論

Use @Mock when unit testing your business logic (only using JUnit and Mockito). Use @MockBean when you write a test that is backed by a Spring Test Context and you want to add or replace a bean with a mocked version of it.

再來看看常較常被使用到的 Mockito 方法

  • when()

測試 function 中,用作指定測試的方法應該進行哪些動作,像是 when(…).thenReturn(…) 或是 when(…).thenThrow(…)

@Mock UserRepository repository;
@Test
public void testMockQuery() {
Mockito.when(repository.findNameById(Mockito.any()))
.thenReturn("Vivi");
}
@Test
public void testMockError() {
Mockito.when(repository.findNameById(Mockito.any()))
.thenThrow(new RuntimeException("mock throw exception"));
}
  • Mockito.verify()

用來驗證行為是否發生

// 模擬創建一個List 
List<Integer> mock = Mockito.mock(List.class);
// 調用mock對象的方法
mock.add(1);
mock.clear();
// 驗證方法是否執行
Mockito.verify(mock).add(1);
Mockito.verify(mock).clear();
  • Mockito.any(xxx.class), Mockito.anyInt(), Mockito.anyString(), Mockito.anyLong()

可以 mock 任意符合的參數,這邊必須要注意:
如果使用了參數匹配,所有的參數都必須要用 Matchers 來匹配,Mockito 繼承 Matchers,anyInt()等均爲 Matchers 方法。

List list = Mockito.mock(List.class);
Mockito.when(list.get(Mockito.anyInt())).thenReturn(1);
Assert.assertEquals(1, list.get(1));
Assert.assertEquals(1, list.get(999));

程式中@Value的Mock方式

若測試程式如下 TestService.java

@Service
@RequiredArgsConstructor
public class TestService {
private final TestMapper testMapper; @Value("${test.key}")
private String key;
@Value("${test.iv}")
private String iv;
public ResultVO testMethod() {
// do something...
}
}

在撰寫 Unit Test 時可以在 service instance 後用 ReflectionTestUtils 在測試時去賦值。

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = WebApplication.class)
class TestServiceT {
private TestService testService;
private final TestMapper mapper = Mockito.mock(TestMapper.class);
@BeforeEach
void init() {
testService = new TestService(mapper);
ReflectionTestUtils.setField(testService, "key", "123");
ReflectionTestUtils.setField(testService, "iv", "test");
}
}

測試時遇過的問題

曾經在使用 Mockito.when(…).thenReturn() 的時候,一直遇到回 null。
當時 mock 的那個 class 方法長得像是:

public class OrderManager() {
public OrderResponse insertOrder(String platformId, ExampleOrder order, String path, String memo) {
// ... do something
}
}

當時速速寫下了這段程式

when(orderManager.insertOrder(Mockito.anyString(), 
Mockito.any(), Mockito.anyString(), Mockito.anyString()
))
.thenReturn(this.mockOrderResponse());

卻發現實際測試在跑到 orderManager.insertOrder() 時一直回傳 null!當下覺得也太詭異了吧!後來將程式改成這樣便運作正常:

when(orderManager.insertOrder(Mockito.any(), Mockito.any(), 
Mockito.any(), Mockito.any()
)).thenReturn(this.mockOrderResponse());

卡了一些時間,這部分找出實際原因會再和大家分享(也歡迎大家跟我分享)

結論

Mockito 是一個非常強大的框架,可以在執行單元測試時幫助我們模擬一個 bean,專注於本身測試的程式,提高單元測試的穩定性。然而 Mockito 還是有些限制要注意:不能 mock static method、不能 mock private method 以及不能 mock final class。

當在撰寫單元測試時,若發現測試很難寫,其實應該要做的是換個角度去想想是否程式在模組切分上面可以更加優化,希望大家都能寫出好維護好測試的程式!

--

--

Vivi Wang
Vivi Wang

Written by Vivi Wang

一些簡單基礎小筆記

No responses yet