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

此篇想談論單元測試並使用 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 物件進行測試

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @SpringBootTest
    class TestStudentService {

    @MockBean
    lateinit var studentDao: StudentDao

    @Autowired
    lateinit var studentServiceImpl: StudentServiceImpl
    }
  3. 加入測試方法

    • 測試取得所有學生資料

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      @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 取得單一學生資料

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @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 欄位取得學生資料

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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)
    }
  • 測試建立學生資料

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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)
    }
  • 測試更新整個學生資料

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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)
    }
  • 測試更新學生信箱

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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)
    }
  • 測試刪除學生資料

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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 供大家參考