前言
本文將使用jest進行測試驅動開發的示例,源碼在github。重點說明在開發中引入單元測試后開發過程,以及測試先行的開發思路。
本文的重點是過程以及思維方法,框架以及用法不是重點。
本文使用的編程語言是javascript,思路對其他語言也是適用的。
本文主要以函數作為測試對象。
環境搭建
假設項目結構為
.
├── README.md
├── package.json
├── src
├── test
└── yarn.lock安裝依賴
yarn add --dev jest打開package.json, 修改scripts字段
"scripts": {
"test": "jest"
}
之后把測試文件放在test文件夾下,使用yarn test 即可查看測試結果
開發
現在要開發一個函數,根據傳入的文件名判斷是否為shell文件。
先做好約定:shell文件應該以 .sh 結尾
shell文件不以 . 開頭
函數為名 isShellFile
下面來看下開發步驟是怎么樣的。
文件初始化
在src目錄下新建 isShellFile.js
touch isShellFile.js
然后一行代碼也不寫,在test目錄下新建 isShellFile.test.js
可以注意到,測試文件的名與源文件名類似,只是中間多了個 .test
touch isShellFile.test.js
第一個用例
打開測試文件 test/isShellFile.test.js ,編寫第一個用例,也是最普通的一個: bash.sh
const isShellFile = require('../src/isShellFile')
test('isShellFile', () => {
// 調用函數,期望它返回值為 true expect(isShellFile('bash.sh')).toBeTruthy()
})
運行 yarn test , 結果如下:
FAIL test/isShellFile.test.js
? isShellFile (2ms)
● isShellFile
TypeError: isShellFile is not a function
^^^
3 | test('isShellFile', () => {
4 |
> 5 | expect(isShellFile('bash.sh')).toBeTruthy()
| ^
6 | })
失敗是意料之中的,因為 src/isShellFile.js 一行代碼也沒寫,所以測試代碼中第5行 isShellFile 無法進行函數調用。
完善源文件src/isShellFile.js
module.exports = function(filename) {
}
這樣 isShellFile 就可以作為函數被調用了。
再運行 yarn test
FAIL test/isShellFile.test.js
? isShellFile (7ms)
● isShellFile
expect(received).toBeTruthy()
^^^
Received: undefined
3 | test('isShellFile', () => {
4 |
> 5 | expect(isShellFile('bash.sh')).toBeTruthy()
| ^
6 | })
又報錯了,但這次報錯原因跟上次不同,說明有進步。
這次報錯原因是,期望函數調用返回值為真 , 但實際沒有返回真 。
這是當然的,因為在源文件中,根本沒有寫返回語句。
為了讓測試通過,修改 src/isShellFile.js
module.exports = function(filename) {
+ return true
}
運行 yarn test , 測試通過了!
PASS test/isShellFile.test.js
? isShellFile (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.548s
Ran all test suites.
把上述修改,提交到版本控制系統中。
git add package.json yarn.lock src test
git commit -m 'feat: init jest test case'
第二個用例
觀察我們的測試用例,發現太簡單了,只有正面的用例,沒有反面的、異常的用例
test('isShellFile', () => {
expect(isShellFile('bash.sh')).toBeTruthy()
})
在 test/isShellFile.test.js 添加一個反面的用例
test('isShellFile', () => {
expect(isShellFile('bash.sh')).toBeTruthy()
+ expect(isShellFile('bash.txt')).toBeFalsy()
})
運行 yarn test
(可以發現,在開發過程中需要反復執行上述命令,有個偷懶的辦法,執行yarn test --watch,即可監聽文件變化,自動執行測試用例)
FAIL test/isShellFile.test.js
? isShellFile (6ms)
● isShellFile
expect(received).toBeFalsy()
^^^
Received: true
4 |
5 | expect(isShellFile('bash.sh')).toBeTruthy()
> 6 | expect(isShellFile('bash.txt')).toBeFalsy()
| ^
7 | })
報錯了,期望返回假,但函數返回的是真。這是因為,源文件中, isShellFile 函數永遠返回真!
完善 src/isShellFile.js 邏輯
module.exports = function(filename) {
- return true;
+ return filename.indexOf('.sh') > -1
};
測試通過了
PASS test/isShellFile.test.js
? isShellFile (4ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.568s
Ran all test suites.
把上述修改提交到版本控制系統
git commit -am 'fix: 函數永遠返回真的bug'
第三個用例
我們再添加一個用例,這次考慮特殊情況: .sh 這種文件,不算是shell文件。
修改 test/isShellFile.test.js
expect(isShellFile("bash.sh")).toBeTruthy();
expect(isShellFile("bash.txt")).toBeFalsy();
+ expect(isShellFile('.sh')).toBeFalsy()
測試不通過
FAIL test/isShellFile.test.js
? isShellFile (8ms)
● isShellFile
expect(received).toBeFalsy()
^^^
Received: true
5 | expect(isShellFile("bash.sh")).toBeTruthy();
6 | expect(isShellFile("bash.txt")).toBeFalsy();
> 7 | expect(isShellFile('.sh')).toBeFalsy()
| ^
8 | });
說明邏輯待完善,修改 src/isShellFile.js
module.exports = function(filename) {
- return filename.indexOf(".sh") > -1;
+ let index = filename.indexOf(".sh");
+ return index > -1 && index != 0;
};
測試通過(為精簡文章內容,后面不再展示測試通過的輸出),提交代碼。
git commit -am 'fix: .sh應該返回false'
第四個用例
按照第三個用例的邏輯, .bash.sh 也不應該是shell文件,那么函數是否能正確判斷呢,測試便知。
修改 test/isShellFile.test.js
expect(isShellFile('.sh')).toBeFalsy()
+ expect(isShellFile('.bash.sh')).toBeFalsy()
測試不通過
FAIL test/isShellFile.test.js
? isShellFile (3ms)
● isShellFile
expect(received).toBeFalsy()
^^^
Received: true
6 | expect(isShellFile("bash.txt")).toBeFalsy();
7 | expect(isShellFile('.sh')).toBeFalsy()
> 8 | expect(isShellFile('.bash.sh')).toBeFalsy()
| ^
9 | });
說明邏輯待完善,修改 src/isShellFile.js
module.exports = function(filename) {
let index = filename.indexOf(".sh");
- return index > -1 && index != 0;
+ return !filename.startsWith('.') && index > -1;
};
測試通過,提交代碼。
git commit -am 'fix: .開頭的文件不算sh文件'
第五個用例
再考慮一種情況,如果 .sh 出現在中間呢?如 bash.sh.txt , 它不應該是shell文件,來看看函數是否能通過測試。
修改 test/isShellFile.test.js
expect(isShellFile('.bash.sh')).toBeFalsy()
+ expect(isShellFile('bash.sh.txt')).toBeFalsy()
測試不通過
FAIL test/isShellFile.test.js
? isShellFile (5ms)
● isShellFile
expect(received).toBeFalsy()
^^^
Received: true
7 | expect(isShellFile('.sh')).toBeFalsy()
8 | expect(isShellFile('.bash.sh')).toBeFalsy()
> 9 | expect(isShellFile('bash.sh.txt')).toBeFalsy()
| ^
10 | });
說明邏輯待完善,修改 src/isShellFile.js
module.exports = function(filename) {
- let index = filename.indexOf(".sh");
- return !filename.startsWith('.') && index > -1;
+ let index = filename.lastIndexOf(".");
+ return !filename.startsWith('.') && filename.substr(index) == '.sh';
};
測試通過,提交代碼。
git commit -am 'fix: .sh必須在結尾'
重構
我們來觀察目前 src/isShellFile.js 的函數邏輯
module.exports = function(filename) {
let index = filename.lastIndexOf(".");
return !filename.startsWith('.') && filename.substr(index) == '.sh';
};
對于 .bashrc 這樣的文件,并不是shell文件,因為它是以 . 開頭的。
則通過 filename.startsWith('.') 判斷即可,前面的函數調用 filename.lastIndexOf(".") 是多余的。也即,目前的函數判斷邏輯不夠簡明。
下面是一種優化思路:
module.exports = function(filename) {
return !filename.startsWith('.') && filename.substr(filename.lastIndexOf(".")) == '.sh';
};
測試通過,提交代碼
git commit -am 'refactor: 優化邏輯'
注意,這個重構示例的重點是:先完成功能,再重構
重構必須要有測試用例,且確保重構后全部測試用例通過
至于其他方面,見仁見智,并不是重點。
結論
本文通過代碼實例,踐行了測試先行的理念。
文中的代碼實現不是重點,而是開發過程。
文中 文件初始化 及 第一個用例 的內容,尤其值得回味,它體現了兩個思路:總是在有一個失敗的單元測試后才開始編碼
用必要的最小代碼讓測試通過
總的來看,TDD總是處于一個循環中:編寫用例
測試失敗
編寫代碼
測試成功
提交代碼
重復以上
通過這樣,功能的實現每次都是最小成本的,功能也是有步驟地、通過迭代完成的,而不是一步登天。
更關鍵的是,完善的測試用例,是開發者的“守護天使”,有了它們,以后在添加新功能時,修改/重構代碼都有了可靠的保障,讓開發者可以充滿信心,code with confidence !
擴展
使用babel
要想使用import/export語法,需要安裝babel相關依賴安裝依賴
yarn add --dev babel-jest @babel/core @babel/preset-env在項目根路徑新增配置文件 babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};重新啟動測試
yarn test --watch
為什么使用jest
因為這是vue官方工具鏈的一部分, 同時也可以為后續的組件測試作準備。