嗨大家好,在寫「鼠年全馬鐵人挑戰-NodeJs轉Golang的爆炸之旅系列」時,其實有時候也會想寫其他東西,所以以後會依照每週不同的想法來撰寫,畢竟40週如果一直寫一個系列,說實在的有點難受,因為這麼長的時間會一直接觸到不同的東西,有時候碰到有趣的東西不能分享是有些痛苦的XD
所以另開了一個系列「鼠年全馬鐵人挑戰-Server的終局之戰系列」
文章也同時發表於medium(`・ω・´)”
關於測試,一直有一張有名的meme圖
為什麼改一下code,會導致debug那麼久呢?最主要是因為副作用-side effect,白話來說就是你改的這些code其實會影響到你預期以外的地方,舉個例子,以下是一個讀json文件的function,並且可以自訂json文件裡面的ps欄位:
const jsonFile = JSON.parse(fs.readFileSync('./文件.json'))
function readFile(ps) {
jsonFile.ps = ps
return jsonFile
}
const one = readFile('第一次讀')
const two = readFile('第二次讀')
console.log(one) //ps欄位也變成'第二次讀'
console.log(two) //ps欄位為'第二次讀'
因為jsonFile是由「外部」引入的,所以在第二次呼叫readFile function的時候其實不知不覺把第一次呼叫的結果one給改了,這就是副作用的一種,那要怎麼解決呢?我們可以把程式碼改成如下:
const jsonFile = JSON.parse(fs.readFileSync('./文件.json'))
function readFile(ps, jsonFile) {
return {...jsonFile, ps: ps}
}
const one = readFile('第一次讀', jsonFile)
const two = readFile('第二次讀', jsonFile)
console.log(one) //ps欄位為'第一次讀'
console.log(two) //ps欄位為'第二次讀'
readFile的jsonFile改為由外部帶入,而readFile裡面的jsonFile也重新copy了這個jsonFile,並重新賦值,這使jsonFile與外部的jsonFile不相依,就不會改到同個變數。
而自從我發生了無數次副作用又在那邊找很久後...
TDD就是
先寫測試,再開發功能,步驟為:
- 先規劃好測試
- 快速開發feature,使feature通過測試
- 依照完成的feature進行重構,重構時可以透過測試審視自己的code是否因重構導致錯誤,所以可以快速重構並且不怕搞爛自己的code
但在許多的RD眼裏,測試一直是拖慢開發速度的一環,主要的原因就是這些新feature測試起來,可能得事先調整很多配置,舉個例子來說,髒沙發辨識AV女優的大致流程如下:
現在有個新功能要將髒沙發的訊息模板由這樣:
改成這樣:
那在開發上很直觀會這樣做:
因為2~5步其實都是「重複性」的測試工作,所以會有人提出「把這幾步寫成自動化測試啊!」的說法,但坦白說,這是件非常麻煩的事情。
首先你要將辨識-server部署好,並且將辨識-server的資料庫與辨識-server連結,最後要確保辨識-server的運作一切正常,才能開始寫「將女優資訊套用到訊息模板上」這段程式。
這導致很多RD會放棄測試,因為不斷重複2~5步的開發雖然攏,但開發上還是比寫測試的時間來的快,甚至覺得所謂「先寫測試在寫程式功能的TDD是個謬論」,但為什麼測試會那麼麻煩呢?主要就是:
測試的所相依的事物太多了
我們回到最一開始,其實我們這個feature的目標是「AV女優面板做調整」,那要測試時我就測「將女優資訊套用到訊息模板上」這段邏輯就好了,辨識-server是可以排除掉的,所以可以把辨識-server的輸入輸出用「假資料」替換掉,即會變成這樣:
而流程就會變成這樣:
可以看到,因為跟其他server都沒有相依,所以寫測試更為容易,這就是我們所謂的單元測試-unit test,大家可以看看測試金字塔,
photo by lawrey medium
越往金字塔的頂端即:
越往金字塔的底部即:
而單元測試即是最底部的,他把許多的外部相依都靠測試替身-test double來排除。
以程式碼來說:
// main.js
const request = require('./request')
const fs = require('fs')
async function searchFace(imagePath) {
return await request({
url: '辨識-server',
formdata: {image: fs.createReadStream(imagePath)}
})
}
async function applyTemplate(imagePath, faceProvider) {
const faceResult = await faceProvider(imagePath)
return {
name: faceResult.name,
description: faceResult.detail.description,
similarity: faceResult.detail.similarity
}
}
// test.js
const main = require('./main')
test('當套用模板時,會回傳正確值', () => {
const mock辨識server = jest.fn().mockReturnValue({
name: '橋本有菜',
detail: {
description: '很可愛',
similarity: '83%'
}
});
expect(main.applyRecognitionTemplate('./test.jpg',mock辨識server).toStrictEqual({
name: '橋本有菜',
description: '很可愛',
similarity: '83%'
})
})
可以看到其實我們利用faceProvider這個參數,讓去call辨識-server的角色有了彈性,我們可以編造假的mock辨識server,它的輸入輸出是由我們測試所定好,並不會因為辨識-server自己出現了什麼問題,而導致我們測試上的誤判。
所以這個測試我們可以focus在applyTemplate function的這段code:
return {
name: faceResult.name,
description: faceResult.detail.description,
similarity: faceResult.detail.similarity
}
因為如果出錯了,一定只有這邊有問題,不可能是辨識server的部分。
這邊我們發現到一個很有趣的現象,就是似乎我們的「程式碼要配合unit test來撰寫」,是的沒錯,假設我們將searchFace function寫在applyTemplate function裡面,
// main.js
const request = require('./request')
const fs = require('fs')
async function searchFace(imagePath) {
return await request({
url: '辨識-server',
formdata: {image: fs.createReadStream(imagePath)}
})
}
async function applyTemplate(imagePath) {
const faceResult = await searchFace(imagePath)
return {
name: faceResult.name,
description: faceResult.detail.description,
similarity: faceResult.detail.similarity
}
}
這時候我們就會面臨無法替換searchFace的現象,因為searchFace被寫死了,這導致與applyTemplate強耦合。
所以程式在寫的時候,其實也要考慮程式的可測試性-testable,這代表:
先上程式碼開發的車再候補測試的票,成本是會越來越大的
因為一開始一直沒考慮可測試性所以後來要測unit test就會遇到頻頻無法隔離的狀況,
當然如果是開發很有經驗的人,就算沒有測試也可以讓各個程式碼的耦合降低,但事先有配合測試,廣義上來說會因測試被動解耦,這是我在寫的時候遇到的有趣現象,
而這邊使用faceProvider是以物件導向SOLID法則的依賴反轉原則來進行設計,依照wiki來說即是
- 高層次的模組不應該依賴於低層次的模組,兩者都應該依賴於抽象介面。
- 抽象介面不應該依賴於具體實現。而具體實現則應該依賴於抽象介面。
聽起來非常的複雜,但其實以這個例子廣義來講就是:
不要把searchFace function寫死在applyTemplate function裡面,不然我要怎麼換他啦ヘ(゜Д、゜)ノ
另外也可以在require的時候引入test double,這也是一種做法
我們可以靠unit test來加快我們寫code驗證迭代的速度,依照「重構|改善既有程式的設計」這本書來說
- 在重構之前,請先準備好堅實的測試程式,測試程式必須是自檢的
- 重構就是用小步驟修改程式,讓輕鬆的找到bug的位置
可以理解到有配合unit test開發,因為減少了一定的debug成本,所以有許多機會是比沒寫測試的人開發來得更快,因為:
但也可以發現,重構此書很多地方都是以「重構」為前提來探討,這代表此feature已經很確定了。
所以,對於功能不明確的prototype產品,是否要先寫測試這就值得思考了,因為feature變動即測試也要修改,所以TDD也不是萬靈丹,任何東西都不是銀彈,我們必須適時且動態的做應對,才是一個好RD
以前一直看不懂SOLID到底在說什麼,而實際碰到之後才發現是一些很直觀的東西,但我也能理解WIKI為什麼要寫得那麼複雜,因為他必須把所有的情況都用兩三句所訴說,我想後面的文章也可以依照我實務碰到的一些問題來描述SOLID,或許可以讓大家更了解~