接續前一篇進行 Service 的測試,我們接序同一個情境針對 Product 的 CRUD,但是應用不同的寫法, 這邊會運用到 Mockito 這個套件來幫助我們應用 Mock 的虛構物件進行測試。
一個系統架構會有多個類別之間互相依賴,且在Spring Boot 中會都註冊成為 Bean,如下圖這樣多個 Class 互相依賴,Class A 需要依賴 B 和 C
引入 mock 測試時,就可以創建假的對象類,替換掉真實的 Class B 和 C,並可以自己設定這個 mock 對象的參數和期望結果,讓我們可以專注在測試當前的 Class A 應該要得到的回傳或是要 Pass 的結果,不受其他的外部服務影響,就能提高測試效率並專注當前測試 Class 的運作。
Java 的 mock 測試框架, Java 中目前主流的 mock 測試工具有 Mockito、JMock、EasyMock..等, SpringBoot 目前內建的是使用 Mockito 框架。和先前 Junit 一樣,只要引用 spring-boot -starter- test 這個 dependency 就包含在裡面。
@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() 來進行,類似作法如下
InOrder inOrder = inOrder(productTestDao);
inOrder.verify(productTestDao, times(1)).findById(id);
inOrder.verify(productTestDao, times(1)).save(any(ProductTest.class));
如果以先前我們測試 ProductService 的部分來畫一個概念圖,原本要測試需要@Autowired ProductDao,並且會直接更動資料庫,如果應用 Mock 將 ProductDao 模擬成 MockProductDao 來模擬和資料庫溝通的操作或回傳,就可以來協助我們進行這些行為的測試。
Mockito 引入測試有兩種寫法:
@ExtendWith(MockitoExtension.class)
MockitoAnnotations.*openMocks*(this)
// 使用 @ExtendWith 或 @BeforeEach 其中之一來初始化 MockitoExtension
// @ExtendWith(MockitoExtension.class)
public class ProductTestServiceTest {
@Mock
private ProductTestDao productTestDao;
@InjectMocks
private ProductTestService productTestService;
@BeforeEach
public void setup() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testReadMockProduct() {
// mockProduct
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());
}
}
相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。