iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0
Software Development

QA 三十天養成日記系列 第 21

[Day21] 軟體世界裡的 TDD/BDD/ATDD!懶人包幫你一次釐清(一)

  • 分享至 

  • xImage
  •  


BDD/TDD/ATDD 我相信在軟體業中並不陌生,但我一直都處於大致了解而已。
今天就一次整理好筆記,區分好他們三者的關係

前幾篇文章中都有提到關於很多【測試】一些概念和種類,大多主要都是以 QA 角色去說明
但今天要說的 BDD/TDD/ATDD 則是不受限於 QA 或開發工程師,因為它是先以【測試】的角度去開發產品

開發團隊寫測試,通常有三種模式:

  • 先寫測試再開發
  • 開發完成再寫測試
  • 完全不寫測試

Q: 什麼?完全不寫測試?你在開玩笑嗎?
A: 這是真的有的喔!但開發工程師大多並不是【不想寫】,而可能是【沒時間寫】測試
因為時程壓力、人力不足 等等多種因素,才造就了這個窘境,但這些原因不是在合理化不寫測試,而是更應該要導入測試才能凸顯產品穩定的重要性。

什麼是 TDD(Test-Driven Development)?

TDD(Test-Driven Development)是一種開發流程,中文是「測試驅動開發」。
用一句白話形容,就是「先寫測試再開發」。
先寫測試除了能確保測試程式的撰寫,還有一個好處:有助於在開發初期釐清程式介面如何設計

它的順序就會是

  1. 產品初期規劃有了完整的需求
  2. 工程師依照需求先撰寫測試
  3. 此時的測試會一定會 fail (因為畢竟還沒開始進行開發寫程式)
  4. 工程師開始著手寫程式,目的是要讓剛寫的測試 pass
  5. 重構程式碼 並 循環以上步驟持續修正的你的程式, 但測試必須保持 pass

TDD 的好處:

  • 有助於減少日後重構時間
  • 可更快速找到 bug 和修正錯誤
  • 更快速的得到回饋及解決問題
  • 開發的程式可以更乾淨並且有更好的設計
  • 提高開發的工作效率
  • 可以建立更好的維護性、擴充性的程式碼

實作階段

將會以兩個面向分別進行,以便於了解其中的差異

  1. (傳統開發模式) 先開發再寫測試
  2. (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;

(傳統開發模式) 先開發再寫測試

Step1. 開發 login API

此部分為 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 介面我們已經完成了。

Step2. 寫測試

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)
})

測試結果:


(TDD) 先寫測試再開發

一樣假設產品功能初期需求已經定義完成,現在則是開發 or QA 先寫測試。
也一樣 account、password 兩個欄位
這時候測試案例可能就會先列出

  • 使用者帳號、密碼皆正確,登入成功 => Check login can work sussces
  • 使用者帳號、密碼皆錯誤,登入成功 => Check login can work fail
  • 使用者帳號錯誤、密碼正確,登入失敗 => Check login account is wrong
  • 使用者帳號正確、密碼錯誤,登入失敗 => Check login password is wrong
  • 使用者帳號輸入空值 => Check login account is no value
  • 使用者帳號輸入不正確的格式 => Check login account is wrong format

其實還有很多測項,暫不細列太多。
主要是想說明,先寫測試時,就會先思考到很多情境,最後開發時,就能寫出更好的 code。

Step1. 寫測試

有了上方的測試案例後

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('帳號格式有誤,請重新輸入')
})

目前因為功能尚未實作,所以測試會全部有錯是正常的,但此時我們已經先將自動化測試的部分撰寫完成了

Step2. 開發 login API

此部分為 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
}

Step3. 再次執行測試,確保 pass


如果得到 fail 表示某處邏輯有錯誤,此時就必須持續修正到測試 pass 為只,目的是確保功能邏輯的正確性。

至此,一個可運作且正確的程式版本已經完成,它涵蓋產品程式和測試程式。

Step4. 重構 code

此階段我就不附上 code 了,就如同字面上意思為 重構
目的是提升 code 的執行效率、可讀性、維護性 等,例如上方的 code 寫了很多 if else if 的判斷條件,或許就能從這方面著手優化。

不過要注意的是,即使再重構過程中,若有測試 fail 的話
那就要會再回到第三步驟,以確保測試通通 pass 才算真正重構完成。

注意: 是修改邏輯層的 code,而非測試案例

總結

  1. (傳統開發模式) 先開發再寫測試
    • 因為這時候的測試也是先以 function 中的邏輯為主,可能測試就會較不全面,較容易有問題,對日後的維護成本也較大,可能一直出現修 A 壞 B 的情況。
  2. (TDD) 先寫測試再開發
    • 這部分測試來說較完整,也比較可以確保穩定性,只是過程中比較耗時,開發時間有可能會增加

我覺得自己經實作後,終於知道為什麼開發有時會說沒時間寫測試了(誤
TDD 屬實比較花費時間,因為必須要將測試案例補齊不是個簡單的事情,但同時它帶來的效益是 穩定
但寫測試也是可以列優先順序的喔!!!
先將重要的測試挑出來,以使用者影響層面最大的案例為優先
這樣的測試效益也會是最大,其餘的細小測試可以日後慢慢補。

沒有絕對的開發模式,只有適合團隊的開發模式

因為大多情況下還要考慮產品的走向、環境資源、人才能力、專案時程等多項評估,才能選擇較適合當前團隊的開發模式。

參考來源

技術部分參考:


上一篇
[Day20][小技巧] 破解圖形驗證碼!使用 Python + 2Captcha 兩步驟就行了!
下一篇
[Day22] 軟體世界裡的 TDD/BDD/ATDD!懶人包幫你一次釐清(二)
系列文
QA 三十天養成日記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言