商品 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() {}
// 取得特定 id 商品
@Test
public void testGetProductById() {}
// 取得特定 id 商品,找不到該 id 之商品
@Test
void testGetProductById_NotFound() {}
// 創建商品
@Test
void testCreateProduct() {}
// 更新商品
@Test
void testUpdateProduct() {}
// 更新商品,找不到該 id 之產品
@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);

// null product name
List<Product> resultNull = productService.searchAndSortProducts(null, "id", "asc", 0, 10);
assertEquals(2, resultNull.size());
assertEquals(mockProduct1, resultNull.get(0));
assertEquals(mockProduct2, resultNull.get(1));

// empty product name
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() {
// Arrange
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());
}

目前訂單部分測試分享到這邊,針對商品部分因為和先前介紹單元測試那邊類似就沒有多去寫,但開發上可以盡量把重要的功能都涵蓋是最好的。