這一篇我們來針對 Service / Dao 層進行測試的撰寫吧,以 MVC 架構下算是 Model 的部分會比較常進行測試程式的撰寫,因為牽涉到主要業務邏輯的運作,所以會比起 Controller 和 View 來說比較常需要進行測試,以 MVC 架構之下我認為個別需要測試的頻率及重點表整理如下
|  | Model(Service/Dao) | Controller | View | 
| 測試頻率 | 高 | 中 | 低 | 
| 重點 | 業務邏輯 |  |  | 
數據驗證
狀態管理 | 請求/錯誤處理驗證
rounting 邏輯
數據轉換(DTO 轉換) | 數據綁定
模板渲染 |
所以這篇來主要針對最高頻率進行測試部分作介紹,Controller 層會應用到的寫法又會不太一樣,之後有機會可以再補充。
單元測試準備
簡單建立一個之前介紹 Jpa 關聯時使用的類似欄位架構
db schema and data
| 12
 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
| 12
 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
| 12
 3
 4
 5
 6
 
 | @Repositorypublic interface ProductTestDao extends JpaRepository<ProductTest, Long> {
 
 public Optional<ProductTest> findByName(String name);
 }
 
 
 | 
ProductTestService
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 
 | @Servicepublic 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 測試撰寫
先加入測試讀取的方法
| 12
 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 引用進來
下面再加入新增、修改、刪除的測試
| 12
 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 這個註解來告知使用自己的資料庫
| 12
 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