BDD/TDD/ATDD 我相信在軟體業中並不陌生,但我一直都處於大致了解而已。
今天就一次整理好筆記,區分好他們三者的關係
前幾篇文章中都有提到關於很多【測試】一些概念和種類,大多主要都是以 QA 角色去說明
但今天要說的 BDD/TDD/ATDD 則是不受限於 QA 或開發工程師,因為它是先以【測試】的角度去開發產品
開發團隊寫測試,通常有三種模式:
Q: 什麼?完全不寫測試?你在開玩笑嗎?
A: 這是真的有的喔!但開發工程師大多並不是【不想寫】,而可能是【沒時間寫】測試
因為時程壓力、人力不足 等等多種因素,才造就了這個窘境,但這些原因不是在合理化不寫測試,而是更應該要導入測試才能凸顯產品穩定的重要性。
TDD(Test-Driven Development)是一種開發流程,中文是「測試驅動開發」。
用一句白話形容,就是「先寫測試再開發」。
先寫測試除了能確保測試程式的撰寫,還有一個好處:有助於在開發初期釐清程式介面如何設計。
它的順序就會是
TDD 的好處:
將會以兩個面向分別進行,以便於了解其中的差異
我們將使用 node.js 的 express 框架來寫 API,之後會再使用 Jest 測試框架進行 API 測試。
功能實作就已最常見的【登入】為主即可
假設產品功能初期需求已經定義完成,開發則直接開始 coding 了
我們已知有個登入功能,有 account、password 兩個欄位要完成
小弟不是專業 backend XDD,接下來可能寫得很粗糙,但還是希望先以了解 TTD 為主即可。
express 部分的檔案,此部分為統一設定 login router
const express = require('express');
const router = express.Router();
const { login } = require('../controllers/login')
/* POST login listing. */
router.post('/login', async function(req, res, next) {
const resp = await login(req, res)
if(resp.status === 200){
res.status(200)
res.end(JSON.stringify({ message: resp.message}))
} else {
res.status(403)
res.send(JSON.stringify({ message: resp.message}))
}
});
module.exports = router;
此部分為 login 主要功能邏輯
//照理應該是要取得 DB 用戶資料,比對是否有此用戶
//但此次主要是以 TDD 練習為主,就先簡單使用 fakeDBDate 當作比對資料
const fakeDBDate = {account:'aaa@bbb.ccc', password:'123456789'}
async function login(req, res){
const account = req.body.account
const password = req.body.password
return (account === fakeDBDate.account && password === fakeDBDate.password) ?
{
status : 200,
message: '登入成功'
} :
{
status : 403,
message: '登入失敗'
}
}
module.exports = {
login
}
實作結果:
目前這個 login API 介面我們已經完成了。
Jest 部分的檔案
const { login } = require('../controllers/login')
test('Check login can work sussces',async ()=>{
const data = {
body:{
account: 'aaa@bbb.ccc',
password: '123456789'
}
}
const resp = await login(data)
expect(resp.status).toBe(200)
})
test('Check login can work fail',async ()=>{
const data = {
body:{
account: 'xxxxx',
password: 'xxxxxx'
}
}
const resp = await login(data)
expect(resp.status).toBe(403)
})
測試結果:
一樣假設產品功能初期需求已經定義完成,現在則是開發 or QA 先寫測試。
也一樣 account、password 兩個欄位
這時候測試案例可能就會先列出
其實還有很多測項,暫不細列太多。
主要是想說明,先寫測試時,就會先思考到很多情境,最後開發時,就能寫出更好的 code。
有了上方的測試案例後
Jest 部分的檔案
const { login } = require('../controllers/login')
const data = {
loginSussces: {
body: { account: 'aaa@bbb.ccc', password: '123456789' }
},
loginFail: {
body: { account: 'xxx@xxx.xxx', password: 'xxxxx' }
},
loginAccountWrong: {
body: { account: 'xxx@xxx.xxx', password: '123456789' }
},
loginPasswordWrong: {
body: { account: 'aaa@bbb.ccc', password: 'xxxxx' }
},
loginNoValue: {
body: { account: '', password: '123456789' }
},
loginWrongFormat: {
body: { account: 'aaa@@@@@@@bbb.ccc@@@', password: '123456789' }
},
};
test('Check login can work sussces',async ()=>{
const resp = await login(data.loginSussces)
expect(resp.status).toBe(200)
expect(resp.message).toBe('登入成功')
})
test('Check login can work fail',async ()=>{
const resp = await login(data.loginFail)
expect(resp.status).toBe(403)
expect(resp.message).toBe('登入失敗')
})
test('Check login account is wrong',async ()=>{
const resp = await login(data.loginAccountWrong)
expect(resp.status).toBe(403)
expect(resp.message).toBe('不存在的帳號,請重新輸入')
})
test('Check login password is wrong',async ()=>{
const resp = await login(data.loginPasswordWrong)
expect(resp.status).toBe(403)
expect(resp.message).toBe('不存在的密碼,請重新輸入')
})
test('Check login account is no value',async ()=>{
const resp = await login(data.loginNoValue)
expect(resp.status).toBe(403)
expect(resp.message).toBe('帳號不可為空值,請重新輸入')
})
test('Check login account is wrong format',async ()=>{
const resp = await login(data.loginNoValue)
expect(resp.status).toBe(403)
expect(resp.message).toBe('帳號格式有誤,請重新輸入')
})
目前因為功能尚未實作,所以測試會全部有錯是正常的,但此時我們已經先將自動化測試的部分撰寫完成了
此部分為 login 主要功能邏輯
//照理應該是要取得 DB 用戶資料,比對是否有此用戶
//但此次主要是以 TDD 練習為主,就先簡單使用 fakeDBDate 當作比對資料
const fakeDBDate = { account:'aaa@bbb.ccc', password:'123456789' }
async function login(req, res){
const account = req.body.account
const password = req.body.password
const emailRule = /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
let resp = {}
if (account === fakeDBDate.account && password === fakeDBDate.password){
resp = {
status : 200,
message: '登入成功'
}
}else if(account === ''){
resp = {
status : 403,
message: '帳號不可為空值,請重新輸入'
}
}else if(!emailRule.test(account)){
resp = {
status : 403,
message: '帳號格式有誤,請重新輸入'
}
}else if(account !== fakeDBDate.account && password === fakeDBDate.password){
resp = {
status : 403,
message: '不存在的帳號,請重新輸入'
}
}else if(account === fakeDBDate.account && password !== fakeDBDate.password){
resp = {
status : 403,
message: '不存在的密碼,請重新輸入'
}
}else{
resp = {
status : 403,
message: '登入失敗'
}
}
return resp
}
module.exports = {
login
}
如果得到 fail 表示某處邏輯有錯誤,此時就必須持續修正到測試 pass 為只,目的是確保功能邏輯的正確性。
至此,一個可運作且正確的程式版本已經完成,它涵蓋產品程式和測試程式。
此階段我就不附上 code 了,就如同字面上意思為 重構
目的是提升 code 的執行效率、可讀性、維護性 等,例如上方的 code 寫了很多 if else if 的判斷條件,或許就能從這方面著手優化。
不過要注意的是,即使再重構過程中,若有測試 fail 的話
那就要會再回到第三步驟,以確保測試通通 pass 才算真正重構完成。
注意: 是修改邏輯層的 code,而非測試案例
我覺得自己經實作後,終於知道為什麼開發有時會說沒時間寫測試了(誤
TDD 屬實比較花費時間,因為必須要將測試案例補齊不是個簡單的事情,但同時它帶來的效益是 穩定。
但寫測試也是可以列優先順序的喔!!!
先將重要的測試挑出來,以使用者影響層面最大的案例為優先
這樣的測試效益也會是最大,其餘的細小測試可以日後慢慢補。
因為大多情況下還要考慮產品的走向、環境資源、人才能力、專案時程等多項評估,才能選擇較適合當前團隊的開發模式。
技術部分參考: