這一篇我們來針對 Service / Dao 層進行測試的撰寫吧,以 MVC 架構下算是 Model 的部分會比較常進行測試程式的撰寫,因為牽涉到主要業務邏輯的運作,所以會比起 Controller 和 View 來說比較常需要進行測試,以 MVC 架構之下我認為個別需要測試的頻率及重點表整理如下
|
Model(Service/Dao) |
Controller |
View |
測試頻率 |
高 |
中 |
低 |
重點 |
業務邏輯 |
|
|
數據驗證
狀態管理 | 請求/錯誤處理驗證
rounting 邏輯
數據轉換(DTO 轉換) | 數據綁定
模板渲染 |
所以這篇來主要針對最高頻率進行測試部分作介紹,Controller 層會應用到的寫法又會不太一樣,之後有機會可以再補充。
單元測試準備
簡單建立一個之前介紹 Jpa 關聯時使用的類似欄位架構
db schema and data
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Entity @Table(name = "product") public class ProductTest {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name; private Double price; private String description;
}
|
ProductTestDao
1 2 3 4 5 6
| @Repository public interface ProductTestDao extends JpaRepository<ProductTest, Long> {
public Optional<ProductTest> findByName(String name); }
|
ProductTestService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @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 測試撰寫
先加入測試讀取的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @SpringBootTest
public class ProductTestServiceTest {
@Autowired private ProductTestDao productTestDao;
@Autowired private ProductTestService productTestService;
@Test public void testReadExistingProduct() { 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 引用進來
下面再加入新增、修改、刪除的測試
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 @Transactional public void testUpdateExistingProduct() { 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() { 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 這個註解來告知使用自己的資料庫
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
| @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) public class ProductTestServiceTest {
@Autowired private ProductTestDao productTestDao;
@Test @Transactional public void testReadExistingProduct() { 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() { 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() { 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 就可以解決這樣的情形。
參考資料:
- Java 工程師必備!Spring Boot 零基礎入門 (hahow 課程)
- JUnit5