這一篇我們來針對 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);
}
}
先加入測試讀取的方法
@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 恢復至原本測試前,可以避免影響資料庫內的資料。
因為剛好在查一些測試相關資料找到這個測試註解的應用,之前撰寫時沒有用過,如果你的測試只是針對 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:
相關文章也會同步更新我的部落格,有興趣也可以在裡面找其他的技術分享跟資訊。