iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0
Software Development

關於我和 Spring Boot 變成家人的那件事系列 第 20

Day 20 - UnitTest (4) - Service/Dao 層測試撰寫

  • 分享至 

  • xImage
  •  

這一篇我們來針對 Service / Dao 層進行測試的撰寫吧,以 MVC 架構下算是 Model 的部分會比較常進行測試程式的撰寫,因為牽涉到主要業務邏輯的運作,所以會比起 Controller 和 View 來說比較常需要進行測試,以MVC 架構之下我認為個別需要測試的頻率及重點表整理如下

Model(Service/Dao) Controller View
測試頻率
重點 業務邏輯
數據驗證
狀態管理 請求/錯誤處理驗證
rounting 邏輯
數據轉換(DTO 轉換) 數據綁定
模板渲染

所以這篇來主要針對最高頻率進行測試部分作介紹,Controller 層會應用到的寫法又會不太一樣,之後有機會可以再補充。

單元測試準備

簡單建立一個之前介紹 Jpa 關聯時使用的類似欄位架構

db schema and data

CREATE TABLE product (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    description VARCHAR(255)
);

INSERT INTO product (name, price, description) VALUES 
('Switch 2', 11999.99, '任天堂熱門的手持遊戲機'),
('PlayStation 5 pro', 24999.99, '索尼的次世代遊戲主機'),
('Xbox Series X', 10999.99, '微軟高效能的遊戲主機'),
('iPhone 16', 26999.99, '蘋果最新型的智慧型手機'),
('Dell XPS 13', 35999.99, '戴爾輕薄高效能的筆記型電腦');

ProductTest Entity

@Entity
@Table(name = "product")
public class ProductTest {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Double price;
    private String description;

    // Getters and Setters
}

ProductTestDao

@Repository
public interface ProductTestDao extends JpaRepository<ProductTest, Long> {

    public Optional<ProductTest> findByName(String name);
}

ProductTestService

@Service
public class ProductTestService {

    @Autowired
    private ProductTestDao productTestDao;

    public ProductTest saveProduct(ProductTest productTest) {
        return productTestDao.save(productTest);
    }

    public Optional<ProductTest> getProductById(Long id) {
        return productTestDao.findById(id);
    }

    public void deleteProduct(Long id) {
        productTestDao.deleteById(id);
    }
}

進行 Service 測試撰寫

先加入測試讀取的方法

@SpringBootTest
//@DataJpaTest
//@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 使用實際的資料庫
public class ProductTestServiceTest {

    @Autowired
    private ProductTestDao productTestDao;

    @Autowired
    private ProductTestService productTestService;

    @Test
    public void testReadExistingProduct() {
        // id = 1 , name = "Switch 2"
        Optional<ProductTest> product = productTestService.getProductById(1L);
        assertTrue(product.isPresent());
        assertEquals(11999.99, product.get().getPrice());
        assertEquals("任天堂熱門的手持遊戲機", product.get().getDescription());
    }
}

仔細看和先前的測試範例不同,我們需要在測試 Class 上面加入 @SpringBootTest 因為先前是直接針對同一包程式內 Class 進行測試,沒有引用到被 Spring Boot 管理的 Bean 進行,但現在要測試的 Service 由於已經註冊為 Bean 交由 SpringBoot 管理,所以需要這個註解告知 SpringBoot 我們才能使用 @Autowired 將 TestProductService 引用進來

下面再加入新增、修改、刪除的測試

@Test
    @Transactional
    public void testUpdateExistingProduct() {
        // id = 2, name = "PlayStation 5 pro"
        Optional<ProductTest> productOptional = productTestService.getProductById(2L);
        assertTrue(productOptional.isPresent());

        ProductTest productTest = productOptional.get();
        productTest.setPrice(23999.99);
        productTest.setDescription("索尼的次世代遊戲主機 - 降價促銷");

        ProductTest updatedProductTest = productTestDao.save(productTest);

        assertEquals(23999.99, updatedProductTest.getPrice());
        assertEquals("索尼的次世代遊戲主機 - 降價促銷", updatedProductTest.getDescription());
    }

    @Test
    @Transactional
    public void testDeleteExistingProduct() {
        // id = 5, name = "Dell XPS 13"
        Optional<ProductTest> productOptional = productTestService.getProductById(5L);
        assertTrue(productOptional.isPresent());

        ProductTest productTest = productOptional.get();
        Long productId = productTest.getId();

        productTestDao.deleteById(productId);

        Optional<ProductTest> deletedProduct = productTestService.getProductById(productId);
        assertFalse(deletedProduct.isPresent());
    }

    @Test
    @Transactional
    public void testCreateNewProduct() {
        // 新增一個新的產品
        ProductTest productTest = new ProductTest();
        productTest.setName("三星 Galaxy S22");
        productTest.setPrice(20999.99);
        productTest.setDescription("三星旗艦智慧型手機");

        ProductTest savedProductTest = productTestService.saveProduct(productTest);

        assertNotNull(savedProductTest);
        assertNotNull(savedProductTest.getId());
        assertEquals("三星 Galaxy S22", savedProductTest.getName());
    }

這邊需要注意有使用到 @Transactional 註解來幫我們進行事務管理,這個註解會在我們進行完該項測試後將所有資料庫有進行的操作都 rollback 恢復至原本測試前,可以避免影響資料庫內的資料。

補充 @DataJpaTest

因為剛好在查一些測試相關資料找到這個測試註解的應用,之前撰寫時沒有用過,如果你的測試只是針對 Jpa 的操作(CRUD)就可以使用,可以輕量化測試 JPA 的功能,只加載相關的資源來進行測試,如果沒有接資料庫也會提供嵌入式資料庫(H2),如果有需要注入 Service, Controller 或是其他非 JPA Repository 層的測試就沒辦法執行。

這邊也提供一個使用這個註解的版本,因為我有加入自己的資料庫,所以需要多使用 @AutoConfigureTestDatabase 這個註解來告知使用自己的資料庫

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 使用實際的資料庫
public class ProductTestServiceTest {

    @Autowired
    private ProductTestDao productTestDao;

    @Test
    @Transactional
    public void testReadExistingProduct() {
        // 假設資料庫已經有 "Switch 2" 這筆資料
        Optional<ProductTest> retrievedProduct = productTestDao.findByName("Switch 2");
        assertTrue(retrievedProduct.isPresent());
        assertEquals(11999.99, retrievedProduct.get().getPrice());
        assertEquals("任天堂熱門的手持遊戲機", retrievedProduct.get().getDescription());
    }

    @Test
    @Transactional
    public void testUpdateExistingProduct() {
        // 假設資料庫已經有 "PlayStation 5 pro" 這筆資料
        Optional<ProductTest> productOptional = productTestDao.findByName("PlayStation 5 pro");
        assertTrue(productOptional.isPresent());

        ProductTest productTest = productOptional.get();
        productTest.setPrice(23999.99);
        productTest.setDescription("索尼的次世代遊戲主機 - 降價促銷");

        ProductTest updatedProductTest = productTestDao.save(productTest);

        assertEquals(23999.99, updatedProductTest.getPrice());
        assertEquals("索尼的次世代遊戲主機 - 降價促銷", updatedProductTest.getDescription());
    }

    @Test
    @Transactional
    public void testDeleteExistingProduct() {
        // 假設資料庫已經有 "Dell XPS 13" 這筆資料
        Optional<ProductTest> productOptional = productTestDao.findByName("Dell XPS 13");
        assertTrue(productOptional.isPresent());

        ProductTest productTest = productOptional.get();
        Long productId = productTest.getId();

        productTestDao.deleteById(productId);

        Optional<ProductTest> deletedProduct = productTestDao.findById(productId);
        assertFalse(deletedProduct.isPresent());
    }

    @Test
    @Transactional
    public void testCreateNewProduct() {
        // 新增一個新的產品
        ProductTest productTest = new ProductTest();
        productTest.setName("三星 Galaxy S22");
        productTest.setPrice(20999.99);
        productTest.setDescription("三星旗艦智慧型手機");

        ProductTest savedProductTest = productTestDao.save(productTest);

        assertNotNull(savedProductTest);
        assertNotNull(savedProductTest.getId());
        assertEquals("三星 Galaxy S22", savedProductTest.getName());
    }
}

這邊大致展示基本要進行 Service 層測試要注意的東西,不過這樣的測試寫法會需要依賴到資料庫操作,以及所有該 Service 需要依賴的其他物件。有時候我們可能還沒完成 dao 或是其他依賴的區域時會沒辦法進行測試,下一篇會介紹到 Mock 就可以解決這樣的情形。


Ref:

  • Java 工程師必備!Spring Boot 零基礎入門 (hahow 課程)
  • JUnit5

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


上一篇
Day 19 - UnitTest (3) - Junit 5 常用註解
下一篇
Day 21 - UnitTest (5) - Mock Test
系列文
關於我和 Spring Boot 變成家人的那件事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言