測試程式碼是軟體開發最重要的方面之一,因為它可以確保產品的品質、可擴展性和可靠性。
但是,如果沒有任何指導,編寫有效的測試可能會很困難。測試程式碼可能比實際功能的程式碼更複雜、更難維護!
原文出處:https://dev.to/one-beyond/the-5-principles-of-unit-testing-1p5f
測試程式碼必須簡單且易於使用。任何查看測試的人都應該立即知道測試的內容及其目的是什麼。開發測試應該以很少的精力和時間投入帶來巨大的價值。
您是否需要超過 30 秒的時間來閱讀和理解您的測試?重寫它!
測試“覆蓋率”是重點嗎?不! 僅測試必要的部份就好。為了敏捷性和簡單性,最好放棄一些測試,只測試主要業務邏輯和主要邊緣情況就好。
不要檢查程式碼中的每一行和內部變數。測試時,您應該專注於結果。即使方法內的程式碼被重構,結果也應該永遠保持不變!
這樣,如果程式碼庫發生更改,您不需要重寫測試。
// Wrong ❌ - Test behaviour
describe('Evaluation Service', () => {
describe('Register Students', () => {
it('Should add new students to the evaluation service', () => {
const studentJosh = {
id: 1,
name: 'Josh McLovin',
average: 6.98,
}
evaluationService.addStudent(studentJosh)
expect(evaluationService._students[0].name).toBe('Josh')
expect(evaluationService._students[0].average).toBe(6.98)
})
})
})
// Right ✅ - Test behaviour
describe('Evaluation Service', () => {
describe('Register Students', () => {
it('Should add new students to the evaluation service', () => {
const studentJosh = {
id: 1,
name: 'Josh McLovin',
average: 6.98,
}
evaluationService.addStudent(studentJosh)
expect(evaluationService.getStudentAverage('Josh')).toBe(6.98)
})
})
})
您是否曾經遇到過名為“它應該[...]正確”的失敗測試,但花了幾分鐘才找到問題所在?
良好命名和結構可以讓您快速且準確地找到任何失敗的測試,最終節省您的寶貴時間。因此,讓我們深入探討兩個關鍵原則,以便在下次測試時牢記:
在命名您的測試時,請嘗試合併以下資訊:
- 正在測試什麼?
- 什麼情況?
- 預期結果是什麼?
// Right ✅ - Test naming
// 1. What is being tested:
describe('Evaluation Service', () => {
describe('Evaluate Students', () => {
// 2 & 3. Context and expected result
it('If the student grade is below the minimum grade, student should be suspended', () => {
const students = [
{ name: 'Mark', grade: 4.25 },
{ name: 'Colin', grade: 6.7 },
{ name: 'Ben', grade: 5.3 },
]
const result = evaluationService.evaluateStudents({ students, minGrade: 5 })
expect(result['Mark']).toBe('suspended')
})
})
})
如果您想維護一個可讀且易於理解的測試套件,請按如下方式建立測試:
安排:設定模擬所需情況所需的所有程式碼。這可以包括初始化變數、模擬響應、實例化被測單元等。
行為:執行正在測試的內容,通常在一行程式碼中。
斷言:檢查得到的結果是否為預期的結果。與上面的一樣,這應該只需要一行。
// Right - AAA Testing Pattern
describe('Evaluation Service', () => {
describe('Average Calculation', () => {
it('Should calculate the average grade of all the students', () => {
// Arrange: create an object with the student names and their grades
const students = [
{ name: 'Mark', grade: 4 },
{ name: 'Colin', grade: 10 },
{ name: 'Ben', grade: 7 },
{ name: 'Tim', grade: 3 },
]
// Act: execute the getAverage method
const avg = evaluationService.getAverage(students)
// Assert: check if the result is the expected one -> (4+10+7+3)/4 = 6
expect(avg).toEqual(6)
})
})
})
如果一個失敗的測試使您的整個套件變紅,那麼您可能沒有以正確的方式處理它!
測試應該獨立和隔離,一次針對並處理一個特定邏輯,從而完成更快、更穩定的測試套件。
如果您不獨立編寫測試會發生什麼?
您將無法找出錯誤和問題的確切原因和位置。
重構測試時,您必須更新和同步多個測試。
您將無法以任何順序執行測試,這可能會導致破壞或跳過某些斷言或期望。
厭倦了在測試中編寫大量可能的輸入?基於屬性的測試可以為您做到這一點!……那是什麼?
基於屬性的測試建立了數百種可能的組合,強調了測試並增加了發現以前未被注意到的錯誤的機會。這種方法甚至可以傳回可能導致意外結果的輸入。
JSVerify 或 Fast-Check 等函式庫提供了促進基於屬性的測試的基本工具。
但是,如果您不想深入進行如此廣泛的測試,那麼盡可能利用真實資料至關重要。像“abc”或“1234”這樣的輸入可能會在實際失敗時錯誤地通過測試。
// Wrong ❌ - False Positive - Test that passes even though it shouldn't
class EvaluationService {
_students = [];
addStudent(student) {
// Add the student if the name has no numbers
if(!student.name.matches(/^([^0-9]*)$/)){
this._students.push(student);
}
}
}
describe('Evaluation Service', () => {
describe('Register Students', () => {
it('Should add students to the Evaluation service', () => {
const mockStudent = {
id: 2,
name: 'username',
average: 7
}
// Won't fail because the name is a string without number -> We are not checking what happens if the user
// inputs a name with a number.
evaluationService.addStudent(mockStudent)
expect(evaluationService.getStudentAverage('username')).toBe(7)
})
})
})
// In the example above, we are making a test so the test passes, instead of looking for edge cases with realistic data
如果您在測試元件中的任何邏輯時遇到困難,這可能反映出您也許應該將元件邏輯分解為更小、更容易且可測試的程式碼片段!
總而言之,遵循此最佳實踐可能會帶來一種新的、可讀且可維護的方法來測試您喜愛的生產程式碼。
經過測試的應用程式才是可靠的應用程式!