商品 Service UnitTest
這邊針對商品部分 Service 寫一些單元測試,下面先列出預計測試的名稱,主要根據實際方法內會出現判斷的條件去設計,嘗試讓每個 Service 單元測試都 100%。
先注入並且初始化我們需要用到的 Bean,這邊主要測試 ProductService ,所以加上 @InjectMocks 註解,其他應用到的 Dao 都用 @Mock 標住用 mock 產生虛擬元件注入有標住 @InjectMocks 的 ProductService,@BeforeEach 先初始化待會會測試用到的一些物件資料。
| 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
 
 | @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 次
 
| 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
 
 | @Testvoid 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 次
 
| 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
 
 | @Testvoid 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 次 (上面兩種條件各一次)
 
| 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
 
 | @Testpublic 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
預計可以拆成下面這些項目:
| 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
 
 | @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 調用次數正確
 
| 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
 
 | @Testvoid 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
- 驗證是否拋出了正確的異常
 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | @Testvoid 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 ,並檢查異常的狀態碼
 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | @Testvoid 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());
 }
 
 | 
目前訂單部分測試分享到這邊,針對商品部分因為和先前介紹單元測試那邊類似就沒有多去寫,但開發上可以盡量把重要的功能都涵蓋是最好的。