➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
Java LoggingJava 測試(二):JUnit

Java 測試(三):Mockito

Continued from previous post:
Java 測試(一):簡介

Table of contents

3 Mockito

Mockito 係基於 proxy 既原理,去做到 mock 一個 method 既 implementation,令到佢變成我地返回想要既結果。
喺 Mockito 既世界裡面,有兩種 objects:
種類描述
Mock當我地用 Mockito 創造一個 mock object,佢既所有 methods 默認會返回 nullfalse0 之類既 default value,或者乜野都唔做(return type 係 void 既話)。
Spy當我地用 Mockito 創造一個 spy object,佢既所有 methods 默認都會用返原本既 method implementations。
Mock 以及 spy 都可以畀我地覆寫佢地既 behaviors,令到我地想佢返回乜野就返回乜野,亦可以令到佢喺唔同既 method calls 返回唔同既野。
除此之外,Mockito 仲畀我地寫 verifications,去驗證到底個 mock object 或者 spy object 畀人 call 左幾多次,咁我地就可以好清楚自己寫既 unit test 所 test 緊既 program flow 或者 scenario 係咪我地所期望既。

3.1 Maven dependencies

如果有引入 spring-boot-starter-test,就已經有 mockito-core 以及 mockito-junit-jupitor
1<dependency> 2 <groupId>org.mockito</groupId> 3 <artifactId>mockito-core</artifactId> 4 <version>4.6.1</version> 5 <scope>test</scope> 6</dependency>
自從 3.4.0,Mockito 就開始支援 mock static methods。不過如非必要,都唔應該 mock static methods,因為有違 OOP 原則;但如果真係需要,就要用以下既 dependency:
1<!-- 建議加埋 mockito-core --> 2 3<dependency> 4 <groupId>org.mockito</groupId> 5 <artifactId>mockito-inline</artifactId> 6 <version>4.6.1</version> 7 <scope>test</scope> 8</dependency>
註:
  • 如果要用 Mockito 去 mock static methods,就唔可以有 PowerMock 既 dependencies,因為會有衝突。
  • 雖然 mockito-inline depends on mockito-core,但最好同時寫埋 mockito-core 既版本,因為如果有用 spring-boot-starter-test,佢會引入特定版本既 mockito-core,而 Mockito 既 libraries 係需要用同一個版本。
參考資料:

3.2 寫 Java code

先建立同配置好一個典型既 Spring Boot + JPA web application,確認連得到 database、成功啟動到,之後寫下面既 code。
Production code:
1@Data 2@Accessors(chain = true) 3@FieldDefaults(level = PRIVATE) 4@Entity 5@Table(name = "system_config") 6public class SystemConfig { 7 @Id 8 @GeneratedValue(strategy = IDENTITY) 9 Long id; 10 11 String configKey; 12 String configValue; 13}
@Repository public interface SystemConfigRepo extends JpaRepository<SystemConfig, Long> { Optional<SystemConfig> findOneByConfigKey(String configKey); }
1@Service 2public class SystemConfigService { 3 4 @Autowired 5 SystemConfigRepo systemConfigRepo; 6 7 public String getConfigValue(String configKey) { 8 return systemConfigRepo.findOneByConfigKey(configKey) 9 .map(SystemConfig::getConfigValue) 10 .orElse(null); 11 } 12}
Test code:
1@RunWith(MockitoJUnitRunner.class) 2public class SystemConfigServiceTest { 3 4 @InjectMocks 5 SystemConfigService systemConfigService; 6 7 @Mock 8 SystemConfigRepo systemConfigRepo; 9 10 @Test 11 public void test_getConfigValue_repoReturnsNull() { 12 13 // given 14 Mockito.when(systemConfigRepo.findOneByConfigKey(ArgumentMatchers.anyString())) 15 .thenReturn(Optional.empty()); 16 17 // when 18 String result = systemConfigService.getConfigValue("timeout"); 19 20 // then 21 Assert.assertNull(result); 22 Mockito.verify(systemConfigRepo, Mockito.times(1)).findOneByConfigKey(ArgumentMatchers.eq("timeout")); 23 } 24 25 @Test 26 public void test_getConfigValue_repoReturnsRecord() { 27 28 // given 29 Mockito.when(systemConfigRepo.findOneByConfigKey(ArgumentMatchers.anyString())) 30 .thenReturn(Optional.of(new SystemConfig() 31 .setConfigKey("timeout") 32 .setConfigValue("1000"))); 33 34 // when 35 String result = systemConfigService.getConfigValue("timeout"); 36 37 // then 38 Assert.assertEquals("1000", result); 39 Mockito.verify(systemConfigRepo, Mockito.times(1)).findOneByConfigKey(ArgumentMatchers.eq("timeout")); 40 } 41}

3.2.1 解釋

因為我地寫左 @RunWith(MockitoJUnitRunner.class),咁呢個 test class 就會用 MockitoJUnitRunner 黎去 run,佢會自動掃瞄 Mockito 既 annotations,然後做相應既處理,例如:
Annotation處理
@MockMockito 會用 cglib(code generation library)去 create 一個 proxy object,而我地可以透過 Mockito.when() 等等既 method 去配置呢個 mock object 將會返回既 output。
@Spy@Mock 一樣,只不過係呢個 annotation 係 for 創造 spy objects 用。
@InjectMocksMockito 會 create 一個 new instance,之後將標註左 @Mock 既 objects 透過 reflection 之類既方式 set 落去被標註 @InjectMocks 既 object 裡面既 fields,就好似 Spring 既 @Autowired 既效果咁。

3.3 Mockito API

3.3.1 唔用 MockitoJUnitRunner 黎 init

我地可能唔想用 MockitoJUnitRunner 黎自動 initialize 個 test class 既 mock fields,原因有可能係因為我地想 @RunWith 用其他既 Runner,咁既話可以喺個 test case 裡面加呢個 method,都可以做到同樣效果:
@Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); // 舊版既寫法 MockitoAnnotations.openMocks(this).close(); // 新版既寫法 }

3.3.2 Mock 有 return type 既 method

如果要 mock 有 return type 既 method,可以用:
Mockito.when(systemConfigRepo.save(ArgumentMatchers.any(SystemConfig.class))) .thenReturn(new SystemConfig());

3.3.3 Mock void method

如果要 mock return type 係 void 既 method,可以用:
Mockito.doNothing() .when(systemConfigRepo) .deleteAll();

3.3.4 驗證 when 階段個 mock method call 左幾多次

如果需要驗證個 method 背後 call 左個 mock method 幾多次又或者有冇 call 到,可以用:
Mockito.verify(systemConfigRepo, Mockito.times(1)) .findOneByConfigKey(ArgumentMatchers.eq("timeout"));

3.3.5 Mock static method

加左 mockito-inline Maven dependency 之後就可以咁樣去 mock 一個 static method:
1@Test 2public void test() { 3 4 // 未 mock,所以 call 緊本來既 method implementation 5 Assert.assertTrue(StringUtils.isBlank("")); 6 7 // mock static 只會喺 try-catch block 裡面生效 8 try (MockedStatic<StringUtils> mock = Mockito.mockStatic(StringUtils.class)) { 9 10 // 因為個 mock 喺初始狀態,所以任何 method calls: 11 // 如果有 return type,都會返回 default value,例如 null 12 // 如果 return type 係 void,就會乜都唔做 13 Assert.assertFalse(StringUtils.isBlank("")); 14 15 // mock:"123" 係咪 blank: 16 // 第一次 call 當係 17 // 第二次 call 唔當係 18 // 第三次及打後既 calls 都當係 19 mock.when(() -> StringUtils.isBlank(ArgumentMatchers.eq("123"))).thenReturn(true, false, true); 20 21 // argument 中左 mock 22 Assert.assertTrue(StringUtils.isBlank("123")); 23 Assert.assertFalse(StringUtils.isBlank("123")); 24 Assert.assertTrue(StringUtils.isBlank("123")); 25 Assert.assertTrue(StringUtils.isBlank("123")); 26 27 // argument 唔中 mock 28 Assert.assertFalse(StringUtils.isBlank("444")); 29 } 30 31 // 因為唔喺個 try-catch block 裡面,所以唔受我地既 mock 影響 32 Assert.assertFalse(StringUtils.isBlank("123")); 33}

3.3.6 重設 mock object

如果需要重設一個 mock object,將之前所有用左 Mockito.when(...) 去 mock 既 methods 都還原到初始狀態,可以用:
Mockito.reset(systemConfigRepo);
註:
  • 根據 Javadoc,一般黎講係唔應該需要用到 Mockito.reset。如果要用到佢,就意味住我地個 test 可能寫得唔好。
    • 因為如果有用以下方法黎喺每個 @Test method 執行之前 initialize mock objects 或者 spy objects,我地係唔需要額外 reset 佢地。
      1. 個 test class 係 @RunWith(MockitoJunitRunner.class)
      2. @Before 或者 @After 既 method 裡面行 MockitoAnnotations.initMocks(this) 或者 MockitoAnnotations.openMocks(this)

3.4 喺 Spring test 裡面 dependency-inject mock 左既 Spring beans

如果我地個 test 係 @SpringBootTest@RunWith(SpringRunner.class),咁佢就會 initialize Spring context,咁 dependency injection 亦都會發生。
spring-boot-starter-test 裡面既 spring-boot-test 提供左一啲 annotations,畀我地去 mock 或者 spy 一個 Spring bean(例如 @Service@Component),並且將佢用喺 dependency injection 裡面,成為 depend on 佢既 Spring beans 既 property value(透過 @Autowired 或者其他 dependency injection 方式)。
當我地用 MockMvc 黎 test controller endpoints 既時候,呢個方式可以幫到我地好容易咁將一啲 mock 左或者 spy 左 Spring beans inject 入去 Spring context 裡面。
我地可以睇下以下既例子。
Bean1.java
@Component public class Bean1 {}
Bean2.java
1@Component 2public class Bean2 { 3 4 @Getter 5 @Autowired 6 Bean1 bean1; 7}
Bean3.java
1@Component 2public class Bean3 { 3 4 @Getter 5 @Autowired 6 Bean2 bean2; 7}
寫 test code:
1@SpringBootTest 2@RunWith(SpringRunner.class) 3public class MockBeanTest { 4 5 @Autowired 6 Bean3 bean3; 7 8 // 如果用既係 @Mock,呢個 test 就會因為 AssertionError 而 fail 9 // 因為 @Mock 只能取代 @InjectMocks 既 objects 裡面既 property values 10 @MockBean 11 Bean1 bean1; 12 13 @Before 14 public void setUp() throws Exception { 15 MockitoAnnotations.openMocks(this).close(); 16 } 17 18 @Test 19 public void test() { 20 Assert.assertEquals(bean1, bean3.getBean2().getBean1()); 21 } 22}

3.5 參考資料

By default, for all methods that return a value, a mock will return either null, a primitive/primitive wrapper value, or an empty collection, as appropriate. For example 0 for an int/Integer and false for a boolean/Boolean.