接續前一篇進行 Service 的測試,我們接序同一個情境針對 Product 的 CRUD,但是應用不同的寫法, 這邊會運用到 Mockito 這個套件來幫助我們應用 Mock 的虛構物件進行測試。
關於 Mock
- 顧名思義 Mock 有模擬、假的意思,將這樣模擬概念應用到測試中,就是希望程式原先依賴的對象都可以透過 mock 方式建立一個假的對象去模擬真實行為。
- 先前介紹單元測試時,有提到特性是應該各單元測試互相獨立,不依賴其他外部系統,Mock Test 是一種單元測試方法,能專注於要測試的程式本身的運作中,避免測試一個方法卻要建構整個 bean 相關的依賴架構。
一個系統架構會有多個類別之間互相依賴,且在 Spring Boot 中會都註冊成為 Bean,如下圖這樣多個 Class 互相依賴,Class A 需要依賴 B 和 C

引入 mock 測試時,就可以創建假的對象類,替換掉真實的 Class B 和 C,並可以自己設定這個 mock 對象的參數和期望結果,讓我們可以專注在測試當前的 Class A 應該要得到的回傳或是要 Pass 的結果,不受其他的外部服務影響,就能提高測試效率並專注當前測試 Class 的運作。

Mockito 套件
Java 的 mock 測試框架, Java 中目前主流的 mock 測試工具有 Mockito、JMock、EasyMock..等, SpringBoot 目前內建的是使用 Mockito 框架。和先前 Junit 一樣,只要引用 spring-boot -starter- test 這個 dependency 就包含在裡面。
測試重點過程及用法
- 建立模擬物件:Mockito 可以建立模擬類別,並且標示模擬類被注入位置
- 控制行為:可定義模擬物件行為,如方法返回值、拋出異常等,且都是我們預期的行為,不須真實運作,可以避免實體運作帶來的影響(資料庫操作資料或實體當下無法運作)
- 驗證互動:可以驗證模擬物件是如我們的預期進行控制行為,驗證互動次數、觸發順序等等。
模擬物件建立
@Mock :標註建立一個模擬物件,通常為資料庫溝通區塊(Dao)或是目前不希望調用真實實體的物件
@InjectMocks :標註模擬物件注入位置,如果今天要測試 Service 會應用到某個 Dao 來進行資料溝通,就是註記在該 Service 類別上,就會把 @Mock 的物件注入
控制行為
使用 when() 這個方法可以控制當特定方法觸發時回傳我們要的結果。
下面是幾種常用的寫法:
when(方法).thenReturn(自訂回傳結果)
when(方法).thenThrow(Exception Class)
doNothing().when(方法)
驗證互動
使用 verify() 來驗證模擬對象的方法是否按照預期被調用。
下面是幾種常用的寫法:
verify(被呼叫類, times(呼叫次數)).被呼叫類使用方法()
如果不寫 times 就是預設驗證被呼叫類呼叫使用方法一次
上面控制和驗證如果需要限制為特定值或是特定類別
when(productRepository.save(any(Product.class))).thenReturn(product);
如果 mock 會被呼叫到的方法有順序驗證,可使用 inOrder 物件來讓 mockito 依照順序驗證
先指定需要驗證的 mock 物件裝入 inOrder,再透過 inOrder.verify() 來進行,類似作法如下
| 12
 3
 4
 
 | InOrder inOrder = inOrder(productTestDao);
 inOrder.verify(productTestDao, times(1)).findById(id);
 inOrder.verify(productTestDao, times(1)).save(any(ProductTest.class));
 
 | 
Mockito 使用注意
- 不能 mock 靜態方法
- 不能 mock private 方法
- 不能 mock final class
Mock 測試應用
如果以先前我們測試 ProductService 的部分來畫一個概念圖,原本要測試需要@Autowired ProductDao,並且會直接更動資料庫,如果應用 Mock 將 ProductDao 模擬成 MockProductDao 來模擬和資料庫溝通的操作或回傳,就可以來協助我們進行這些行為的測試。

Mockito 引入測試有兩種寫法:
- 測試類要加上 @ExtendWith(MockitoExtension.class)
- 每個測試類都要初始化,所以可以用 @BeforeEach 配合初始化 MockitoAnnotations.*openMocks*(this)
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 
 | 
 public class ProductTestServiceTest {
 
 @Mock
 private ProductTestDao productTestDao;
 
 @InjectMocks
 private ProductTestService productTestService;
 
 @BeforeEach
 public void setup() {
 MockitoAnnotations.openMocks(this);
 }
 
 @Test
 public void testReadMockProduct() {
 
 ProductTest mockProduct = new ProductTest();
 mockProduct.setId(1L);
 mockProduct.setName("Switch 2");
 mockProduct.setPrice(11999.99);
 mockProduct.setDescription("任天堂熱門的手持遊戲機");
 
 when(productTestDao.findById(1L)).thenReturn(Optional.of(mockProduct));
 
 Optional<ProductTest> product = productTestService.getProductById(1L);
 assertTrue(product.isPresent());
 assertEquals(11999.99, product.get().getPrice());
 assertEquals("任天堂熱門的手持遊戲機", product.get().getDescription());
 verify(productTestDao, times(1)).findById(1L);
 }
 
 @Test
 public void testUpdateMockProduct() {
 Long id = 2L;
 ProductTest mockProduct = new ProductTest();
 mockProduct.setId(id);
 mockProduct.setName("PlayStation 5 pro discount");
 mockProduct.setPrice(21999.99);
 mockProduct.setDescription("索尼的次世代遊戲主機 - 降價促銷");
 
 when(productTestDao.findById(id)).thenReturn(Optional.of(mockProduct));
 when(productTestDao.save(any(ProductTest.class))).thenReturn(mockProduct);
 ProductTest updatedMockProduct = productTestService.updateProductById(id, mockProduct);
 
 assertNotNull(updatedMockProduct);
 assertEquals(id, updatedMockProduct.getId());
 assertEquals("PlayStation 5 pro discount", updatedMockProduct.getName());
 assertEquals(21999.99, updatedMockProduct.getPrice());
 
 InOrder inOrder = inOrder(productTestDao);
 
 inOrder.verify(productTestDao, times(1)).findById(id);
 inOrder.verify(productTestDao, times(1)).save(any(ProductTest.class));
 }
 
 @Test
 public void testDeleteMockProduct() {
 doNothing().when(productTestDao).deleteById(1L);
 productTestService.deleteProductById(1L);
 verify(productTestDao, times(1)).deleteById(1L);
 }
 
 @Test
 public void testCreateMockProduct() {
 
 ProductTest mockProduct = new ProductTest();
 mockProduct.setId(6L);
 mockProduct.setName("三星 Galaxy S22");
 mockProduct.setPrice(20999.99);
 mockProduct.setDescription("三星旗艦智慧型手機");
 
 when(productTestDao.save(mockProduct)).thenReturn(mockProduct);
 
 ProductTest savedProduct = productTestService.saveProduct(mockProduct);
 
 assertNotNull(savedProduct);
 assertNotNull(savedProduct.getId());
 assertEquals("三星 Galaxy S22", savedProduct.getName());
 }
 }
 
 | 
參考資料: