iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
0
Modern Web

30天從零撰寫 Kotlin 語言並應用於 Spring Boot 開發系列 第 22

[Day 22] 遠征 Kotlin × Spring Boot 介紹單元測試 (1)

  • 分享至 

  • xImage
  •  

此篇想談論單元測試並使用 Junit 工具進行測試撰寫,單元測試是針對程式模組(軟體設計的最小單位)進行正確性檢驗的測試工作,並且是一段可自動化執行的程式碼,程式會呼叫被測試的工作單元,再針對此單元所執行的最終結果進行假設驗證,驗證此單元結果是否符合我們所預期的行為,而工作單元通常是程式模組最小的單位,當單元測試檢測發現程式錯誤時,我們也可以在第一時間進行修正,已證實程式達到專案需求目標,故單元測試應該具備以下特質:

  • 它應該是自動化,而且可被重複執行的
  • 它應該很容易被實現
  • 它的存在對於專案是具有意義的,並非臨時性作用
  • 它的執行應該是容易的
  • 它應該要能完全掌握被測試的單元
  • 它應該是能完全被隔離的,執行時獨立於其他測試
  • 如果檢測驗證失敗時,應該要能清楚呈現期望值與實際值差異,並且要能很清楚知道發生的原因為何,進一步修正錯誤

好的單元測試,應該要具備三種特色:

  • 可信賴性(Trustworthiness)

    開發者應對自己所撰寫測試的結果有信心,並且是針對實際專案需求進行正確的測試

  • 可維護性(Maintainability)

    測試也應保持好的可維護性,無法維護的測試會是一場惡夢,只會導致拖累專案整體進度

  • 可閱讀性(Readability)

    每次修改程式時都會持續進行單元測試檢測,當測試發生問題時,為了快速找到癥結點所在,保持好的閱讀性相當重要。

而實際在測試方法撰寫中,我們可以採取 3A 測試原則,如下:

  1. Arrange 初始化目標物件、相依物件、方法參數、預期結果
  2. Act 執行測試工作單元,取得實際測試結果
  3. Assert 驗證結果是否符合預期結果

以下直接將先前的 RESTful API 範例撰寫 Service Unit Test:

  1. Spring Boot 在建置專案時已經先引入 Test 套件org.springframework.boot:spring-boot-starter-test,裡面會包含相關測試模組,如 Junit、AssertJ、Mockito等元件

  2. 測試類別設定參數(@SpringBootTest、@MockBean、@Autowired):

    @SpringBootTest Annotation 會為我們引入測試元件

    @MockBean 則是要新增一個 DAO 假物件,幫助我們順利進行Service的單元測試

    @Autowired 新增一個 Service 物件進行測試

    @SpringBootTest
    class TestStudentService {
    
        @MockBean
        lateinit var studentDao: StudentDao
    
        @Autowired
        lateinit var studentServiceImpl: StudentServiceImpl
    }
    
  3. 加入測試方法

    • 測試取得所有學生資料

          @Test
          fun shouldGetAllStudentWhenCallMethod() {
              // Arrange 初始化測試資料與預期結果
              val expectedResult : MutableList<Student> = mutableListOf<Student>()
              expectedResult.add(Student(1, "Devin", "devin@gmail.com"))
              expectedResult.add(Student(2, "Eric", "eric@gmail.com"))
              given(studentDao.findAll()).willReturn(expectedResult)
      
              // Act 執行測試工作單元,取得實際測試結果
              val actual : MutableList<Student> = studentServiceImpl.findAllStudent()
      
              // Assert 驗證結果是否符合預期結果
              assertEquals(expectedResult, actual)
          }
      
    • 測試利用 id 取得單一學生資料

          @Test
          fun shouldGetOneStudentWhenCallMethodById() {
              val expectedResult = Student(1, "Devin", "devin@gmail.com")
              given(studentDao.findById(1)).willReturn(expectedResult)
      
              val actual : Student? = studentServiceImpl.findByStudentId(1)
      
              assertEquals(expectedResult, actual)
          }
      
  • 測試利用 Name 欄位取得學生資料

        @Test
        fun shouldGetStudentsWhenCallMethodByName() {
            val expectedResult : MutableList<Student> = mutableListOf<Student>()
            expectedResult.add(Student(1, "Devin", "devin@gmail.com"))
            given(studentDao.findByName("Devin")).willReturn(expectedResult)
    
            val actual : MutableList<Student> = studentServiceImpl.findByStudentName("Devin")
    
            assertEquals(expectedResult, actual)
        }
    
  • 測試建立學生資料

        @Test
        fun shouldGetNewStudentWhenCallMethodByStudent() {
            val expectedResult = Student( 1, "Devin", "devin@gmail.com")
            val requestParameter = Student( name = "Devin", email = "devin@gmail.com")
            given(studentDao.save(requestParameter)).willReturn(expectedResult)
    
            val actual : Student = studentServiceImpl.addStudent(requestParameter)
    
            assertEquals(expectedResult, actual)
        }
    
  • 測試更新整個學生資料

        @Test
        fun shouldUpdatedStudentWhenCallMethodByStudent() {
            val expectedResult = Student(1, "Devin", "devin@gmail.com")
            val requestParameter = Student(1, "Eric", "eric@gmail.com")
            given(studentDao.save(requestParameter)).willReturn(expectedResult)
    
            val actual : Student? = studentServiceImpl.updateStudent(requestParameter)
    
            assertEquals(expectedResult, actual)
        }
    
  • 測試更新學生信箱

        @Test
        fun shouldUpdatedEmailWhenCallMethodByStudent() {
            val expectedResult = Student(1, "Devin", "devin@gmail.com")
            val requestParameter = Student(1, "Devin", "test@gmail.com")
            given(studentDao.save(requestParameter)).willReturn(expectedResult)
    
            val actual : Student? = studentServiceImpl.updateStudentEmail(requestParameter)
    
            assertEquals(expectedResult.email, actual?.email)
        }
    
  • 測試刪除學生資料

        @Test
        fun shouldDeletedStudentWhenCallMethodByStudent() {
            val expectedResult = true
            val expectedSaveResult = Student(1, "Devin", "devin@gmail.com")
            given(studentDao.findById(1)).willReturn(expectedSaveResult)
    
            val actual = studentServiceImpl.deleteStudent(1)
    
            assertEquals(expectedResult, actual)
        }
    

此文章有提供範例程式碼在 Github 供大家參考


上一篇
[Day 21] 遠征 Kotlin × Spring Boot 爬蟲實戰教學
下一篇
[Day 23] 遠征 Kotlin × Spring Boot 介紹單元測試 (2)
系列文
30天從零撰寫 Kotlin 語言並應用於 Spring Boot 開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言