iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0

商品 Service UnitTest

這邊針對商品部分 Service 寫一些單元測試,下面先列出預計測試的名稱,主要根據實際方法內會出現判斷的條件去設計,嘗試讓每個 Service 單元測試都 100%。

先注入並且初始化我們需要用到的Bean,這邊主要測試 ProductService ,所以加上 @InjectMocks 註解,其他應用到的 Dao 都用 @Mock 標住用 mock 產生虛擬元件注入有標住 @InjectMocks 的 ProductService,@BeforeEach 先初始化待會會測試用到的一些物件資料。

@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 次
  	@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次
@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 次 (上面兩種條件各一次)
@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

預計可以拆成下面這些項目:

@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 調用次數正確
@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
    • 驗證是否拋出了正確的異常
@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 ,並檢查異常的狀態碼
@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());
    }

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


相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。


上一篇
Day 27 - 電商 RESTFul API + Spring Security (2) 訂單功能
下一篇
Day 29 - Swagger UI
系列文
關於我和 Spring Boot 變成家人的那件事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言