商品 Service UnitTest
這邊針對商品部分 Service 寫一些單元測試,下面先列出預計測試的名稱,主要根據實際方法內會出現判斷的條件去設計,嘗試讓每個 Service 單元測試都 100%。
先注入並且初始化我們需要用到的 Bean,這邊主要測試 ProductService
,所以加上 @InjectMocks
註解,其他應用到的 Dao 都用 @Mock
標住用 mock 產生虛擬元件注入有標住 @InjectMocks
的 ProductService,@BeforeEach 先初始化待會會測試用到的一些物件資料。
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
| @ExtendWith(MockitoExtension.class) public class ProductServiceTest { @Mock private ProductDao productDao;
@InjectMocks private ProductService productService;
private Product mockProduct1; private Product mockProduct2; private ProductRequest mockProductRequest; private List<Product> mockProducts;
@BeforeEach void setUp() { mockProduct1 = new Product(); mockProduct1.setId(1); mockProduct1.setProductName("Test Product"); mockProduct1.setUnitPrice(10.0); mockProduct1.setUnitsInStock(100); mockProduct1.setDiscontinued(false); Supplier supplier1 = new Supplier(); supplier1.setId(1); mockProduct1.setSupplier(supplier1);
mockProduct2 = new Product(); mockProduct2.setId(2); mockProduct2.setProductName("Test Wireless Mouse"); mockProduct2.setDiscontinued(false); Supplier supplier2 = new Supplier(); supplier2.setId(2); mockProduct2.setSupplier(supplier2); mockProduct2.setUnitPrice(24.99); mockProduct2.setUnitsInStock(103);
mockProductRequest = new ProductRequest(); mockProductRequest.setProductName("New/Update Product"); mockProductRequest.setUnitPrice(15.0); mockProductRequest.setUnitsInStock(50); mockProductRequest.setDiscontinued(false); mockProductRequest.setSupplier(supplier1); }
@Test public void testGetAllProducts() {} @Test public void testGetProductById() {} @Test void testGetProductById_NotFound() {} @Test void testCreateProduct() {} @Test void testUpdateProduct() {} @Test void testUpdateProduct_NotFound() {} @Test void testDeleteProduct() {} @Test void testSearchAndSortProducts() {} @Test void testSearchAndSortProducts_NullOrEmptyProductName() {} }
|
關於基本查詢相關
testGetAllProducts
- 模擬回傳 mockProduct1, mockProduct2
- 驗證執行後回傳的第 1 個 product 資訊和 mockProduct1 相同
- 驗證執行後回傳的第 2 個 product 資訊和 mockProduct2 相同
- 驗證回傳數量
- 驗證調用 findAll() 1 次
testGetProductById
- 模擬查尋 id = 1 回傳 mockProduct1
- 驗證執行後回傳的 product 資訊和 mockProduct1 相同
- 驗證調用 findById() 1 次
testGetProductById_NotFound
- 模擬查尋 id = 3 回傳空
- 驗證執行後 product 是否存在為 false
- 驗證調用 findById() 1 次
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
| @Test void testSearchAndSortProducts() { mockProducts = Arrays.asList(mockProduct2, mockProduct1); Page<Product> productPage = new PageImpl<>(mockProducts);
when(productDao.findByProductNameContainingIgnoreCase(eq("Test"), any(PageRequest.class))) .thenReturn(productPage);
List<Product> result = productService.searchAndSortProducts("Test", "id", "desc", 0, 10);
assertEquals(2, result.size()); assertEquals(mockProduct2, result.get(0)); assertEquals(mockProduct1, result.get(1)); verify(productDao).findByProductNameContainingIgnoreCase(eq("Test"), any(PageRequest.class)); }
@Test void testSearchAndSortProducts_NullOrEmptyProductName() { mockProducts = Arrays.asList(mockProduct1, mockProduct2); Page<Product> productPage = new PageImpl<>(mockProducts); when(productDao.findAll(any(PageRequest.class))).thenReturn(productPage);
List<Product> resultNull = productService.searchAndSortProducts(null, "id", "asc", 0, 10); assertEquals(2, resultNull.size()); assertEquals(mockProduct1, resultNull.get(0)); assertEquals(mockProduct2, resultNull.get(1));
List<Product> resultEmpty = productService.searchAndSortProducts("", "id", "asc", 0, 10); assertEquals(2, resultEmpty.size()); assertEquals(mockProduct1, resultEmpty.get(0)); assertEquals(mockProduct2, resultNull.get(1));
verify(productDao, times(2)).findAll(any(PageRequest.class)); }
|
關於新增、刪除、修改相關
testCreateProduct
- 根據初始化的 mockProductRequest 模擬儲存,回傳 mockProductRequest 轉成的 newProduct
- 驗證回傳不為空
- 驗證回傳 product 和 newProduct 相同
- 驗證調用 save() 1 次
testUpdateProduct
- 根據初始化的 mockProductRequest 模擬更新,回傳 mockProductRequest 轉成的 updateProduct,模擬查詢 id =1 然後更新成 updateProduct
- 驗證回傳 product 和 updateProduct 相同
- 驗證調用 findById() 1 次
- 驗證調用 save() 1 次
testUpdateProduct_NotFound
- 模擬查詢 id = 3 商品不存在回傳空
- 驗證調用 findById() 1 次
- 驗證不調用到 save()
testDeleteProduct
- 模擬刪除 id =1 商品不回傳值
- 驗證調用 deleteById 1 次
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
| @Test void testCreateProduct() { Product newMockProduct = productService.convertToModel(mockProductRequest);
when(productDao.save(any(Product.class))).thenReturn(newMockProduct);
Product createdProduct = productService.createProduct(mockProductRequest);
assertNotNull(createdProduct); assertEquals("New/Update Product", createdProduct.getProductName()); verify(productDao, times(1)).save(any(Product.class)); }
@Test void testUpdateProduct() { Product updateMcokProduct = productService.convertToModel(mockProductRequest);
when(productDao.findById(1)).thenReturn(Optional.of(mockProduct1)); when(productDao.save(any(Product.class))).thenReturn(updateMcokProduct);
Product updatedProduct = productService.updateProduct(1, mockProductRequest);
assertNotNull(updatedProduct); assertEquals("New/Update Product", updatedProduct.getProductName()); verify(productDao, times(1)).findById(1); verify(productDao, times(1)).save(any(Product.class)); }
@Test void testUpdateProduct_NotFound() { when(productDao.findById(3)).thenReturn(Optional.empty());
Product updatedProduct = productService.updateProduct(3, mockProductRequest);
assertNull(updatedProduct); verify(productDao, times(1)).findById(3); verify(productDao, never()).save(any(Product.class));
}
@Test void testDeleteProduct() { doNothing().when(productDao).deleteById(1);
productService.deleteProductById(1);
verify(productDao, times(1)).deleteById(1); }
|
搜尋相關
testSearchAndSortProducts
- 模擬查詢 ‘Test’ 回傳 mockProduct1, mockProduct2
- 驗證回傳 2 筆數
- 驗證執行後為降序排列
- 驗證調用查詢 1 次
testSearchAndSortProducts_NullOrEmptyProductName
- 模擬不帶任何 productName 查詢仍回傳 mockProduct1, mockProduct2
- product == null,驗證回傳 mockProduct1, mockProduct2 兩筆
- product == “”,驗證回傳 mockProduct1, mockProduct2 兩筆
- 驗證調用查詢 2 次 (上面兩種條件各一次)
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
| @Test public void testGetAllProducts() { mockProducts = Arrays.asList(mockProduct1, mockProduct2); when(productDao.findAll()).thenReturn(mockProducts);
List<Product> products = productService.getAllProducts();
assertEquals(products.get(0).getProductName(), "Test Product"); assertEquals(products.get(1).getProductName(), "Test Wireless Mouse"); assertTrue(products.size() == 2); verify(productDao, times(1)).findAll(); }
@Test public void testGetProductById() { when(productDao.findById(1)).thenReturn(Optional.of(mockProduct1));
Optional<Product> product = productService.getProductById(1);
assertTrue(product.isPresent()); assertEquals(product.get().getProductName(), "Test Product"); assertEquals(product.get().getUnitPrice(), 10.0); assertEquals(product.get().getUnitsInStock(), 100); verify(productDao, times(1)).findById(1); }
@Test void testGetProductById_NotFound() { when(productDao.findById(3)).thenReturn(Optional.empty());
Optional<Product> product = productService.getProductById(3);
assertFalse(product.isPresent()); verify(productDao, times(1)).findById(3); }
|
訂單 Service UnitTest
再來針對新建訂單部分 Service 的單元測試
盡量讓內部邏輯可以都被測驗到,每個方法裡的判斷都有走過一遍,這樣測試覆蓋率高也能確保程式運作正常。
先注入並且初始化我們需要用到的 Bean,這邊主要測試 OrderService
,所以加上 @InjectMocks
註解,其他應用到的 Dao 都用 @Mock
標住用 mock 產生虛擬元件注入有標住 @InjectMocks
的 OrderService
預計可以拆成下面這些項目:
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
| @ExtendWith(MockitoExtension.class) public class OrderServiceTest { @InjectMocks private OrderService orderService;
@Mock private OrderInfoDao orderInfoDao;
@Mock private OrderItemDao orderItemDao;
@Mock private ProductDao productDao;
@Test void testCreateOrder_Success() { }
@Test void testCreateOrder_NotFoundProduct() { }
@Test void testCreateOrder_InsufficientStock() { } }
|
createOrder_Success
: 測試正確創建訂單
- 模擬每個有被呼叫到的 dao 都回應我們預期的結果
- 驗證最後有回傳 response
- 驗證庫存確實有被更新
- 呼叫到的 dao 調用次數正確
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
| @Test void testCreateOrder_Success() { Integer userId = 1; CreateOrderInfoRequest request = new CreateOrderInfoRequest(); BuyItem buyItem = new BuyItem(); buyItem.setProductId(1); buyItem.setQuantity(2); request.setBuyItemList(Arrays.asList(buyItem));
Product product = new Product(); product.setId(1); product.setProductName("Test Product"); product.setUnitPrice(10.0); product.setUnitsInStock(5);
OrderInfo orderInfo = new OrderInfo(); orderInfo.setId(1); orderInfo.setUserId(userId); orderInfo.setTotalAmount(20.0);
OrderItem orderItem = new OrderItem(); orderItem.setId(1); orderItem.setOrderInfoId(1); orderItem.setProductId(1); orderItem.setQuantity(2); orderItem.setAmount(10.0);
when(productDao.findById(1)).thenReturn(Optional.of(product)); when(orderInfoDao.save(any())).thenReturn(orderInfo); when(orderItemDao.saveAll(any())).thenReturn(Arrays.asList(orderItem));
CreateOrderResponse response = orderService.createOrder(userId, request);
assertNotNull(response); verify(productDao, times(1)).save(any()); verify(productDao).save(argThat(savedProduct -> savedProduct.getId().equals(1) && savedProduct.getUnitsInStock() == 3 ));
verify(orderInfoDao, times(1)).save(any()); verify(orderItemDao, times(1)).saveAll(any()); }
|
createOrder_ProductNotFound
: 測試訂單內商品不存在
- 模擬
ProductDao
返回空的 Optional
- 驗證是否拋出了正確的異常
1 2 3 4 5 6 7 8 9 10 11 12
| @Test void testCreateOrder_ProductNotFound() { Integer userId = 1; CreateOrderInfoRequest request = new CreateOrderInfoRequest(); BuyItem buyItem = new BuyItem(); buyItem.setProductId(1); buyItem.setQuantity(2); request.setBuyItemList(Arrays.asList(buyItem));
when(productDao.findById(1)).thenReturn(Optional.empty()); assertThrows(ResponseStatusException.class, () -> orderService.createOrder(userId, request)); }
|
createOrder_InsufficientStock
: 測試訂單內商品目前庫存不足
- 模擬產品庫存量小於請求的數量
- 驗證是否拋出 ResponseStatusException ,並檢查異常的狀態碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Test void testCreateOrder_InsufficientStock() { Integer userId = 1; CreateOrderInfoRequest request = new CreateOrderInfoRequest(); BuyItem buyItem = new BuyItem(); buyItem.setProductId(1); buyItem.setQuantity(10); request.setBuyItemList(Arrays.asList(buyItem));
Product product = new Product(); product.setId(1); product.setProductName("Test Product"); product.setUnitPrice(10.0); product.setUnitsInStock(1);
when(productDao.findById(1)).thenReturn(Optional.of(product));
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () -> orderService.createOrder(userId, request)); assertEquals(HttpStatus.BAD_REQUEST, exception.getStatusCode()); }
|
目前訂單部分測試分享到這邊,針對商品部分因為和先前介紹單元測試那邊類似就沒有多去寫,但開發上可以盡量把重要的功能都涵蓋是最好的。