此篇想談論單元測試並使用 Junit
工具進行測試撰寫,單元測試是針對程式模組(軟體設計的最小單位)進行正確性檢驗的測試工作,並且是一段可自動化執行的程式碼,程式會呼叫被測試的工作單元,再針對此單元所執行的最終結果進行假設驗證,驗證此單元結果是否符合我們所預期的行為,而工作單元通常是程式模組最小的單位,當單元測試檢測發現程式錯誤時,我們也可以在第一時間進行修正,已證實程式達到專案需求目標,故單元測試應該具備以下特質:
- 它應該是自動化,而且可被重複執行的
- 它應該很容易被實現
- 它的存在對於專案是具有意義的,並非臨時性作用
- 它的執行應該是容易的
- 它應該要能完全掌握被測試的單元
- 它應該是能完全被隔離的,執行時獨立於其他測試
- 如果檢測驗證失敗時,應該要能清楚呈現期望值與實際值差異,並且要能很清楚知道發生的原因為何,進一步修正錯誤
好的單元測試,應該要具備三種特色:
可信賴性(Trustworthiness)
開發者應對自己所撰寫測試的結果有信心,並且是針對實際專案需求進行正確的測試
可維護性(Maintainability)
測試也應保持好的可維護性,無法維護的測試會是一場惡夢,只會導致拖累專案整體進度
可閱讀性(Readability)
每次修改程式時都會持續進行單元測試檢測,當測試發生問題時,為了快速找到癥結點所在,保持好的閱讀性相當重要。
而實際在測試方法撰寫中,我們可以採取 3A
測試原則,如下:
Arrange
初始化目標物件、相依物件、方法參數、預期結果Act
執行測試工作單元,取得實際測試結果Assert
驗證結果是否符合預期結果
以下直接將先前的 RESTful API 範例撰寫 Service Unit Test:
Spring Boot 在建置專案時已經先引入 Test 套件org.springframework.boot:spring-boot-starter-test,裡面會包含相關測試模組,如 Junit、AssertJ、Mockito等元件
測試類別設定參數(@SpringBootTest、@MockBean、@Autowired):
@SpringBootTest Annotation 會為我們引入測試元件
@MockBean 則是要新增一個 DAO 假物件,幫助我們順利進行Service的單元測試
@Autowired 新增一個 Service 物件進行測試
1
2
3
4
5
6
7
8
9
class TestStudentService {
lateinit var studentDao: StudentDao
lateinit var studentServiceImpl: StudentServiceImpl
}加入測試方法
測試取得所有學生資料
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
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
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
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
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
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
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 供大家參考