Table of contents
3 Mockito
Mockito 係基於 proxy 既原理,去做到 mock 一個 method 既 implementation,令到佢變成我地返回想要既結果。
喺 Mockito 既世界裡面,有兩種 objects:
種類 | 描述 |
---|
Mock | 當我地用 Mockito 創造一個 mock object,佢既所有 methods 默認會返回 null 、false 、0 之類既 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 | 處理 |
---|
@Mock | Mockito 會用 cglib(code generation library)去 create 一個 proxy object,而我地可以透過 Mockito.when() 等等既 method 去配置呢個 mock object 將會返回既 output。 |
@Spy | 同 @Mock 一樣,只不過係呢個 annotation 係 for 創造 spy objects 用。 |
@InjectMocks | Mockito 會 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 佢地。
- 個 test class 係
@RunWith(MockitoJunitRunner.class)
- 喺
@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.