接續前一篇進行 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() 來進行,類似作法如下
1 2 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)
1 2 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()); } }
|
參考資料: