本文序
回顧目前語法章節的部分我們已經學了有關於:
- 測試案例與情境:
describe
、it
- 測試案例中 Setup 與 Teardown 環節有關的語法:
beforeEach
、beforeAll
、afterEach
與 beforeAll
- 測試斷言中的各種
Matchers
與快照
而基於上述幾個語法加上先前的測試概念,我們已經滿足了撰寫基本單元測試的條件了!所以現在是⋯⋯測驗時間!!!
在測驗章節我們主要流程將會是:
- 閱讀故事與題目,釐清需求與規則。
- 規劃測試情境與測試案例,並列好描述的部分。
- 撰寫測試程式碼(Testing Code),並執行測試時得到測試案例失敗。
- 按照題目要求完成產品程式碼(Production Code)
- 再次執行測試確保測試通過。
而本文撰寫測試程式碼與產品程式碼的環節我也會提供 demo 的範例供參考與撰寫思路,那麼就讓我們開始吧!
故事
「柯基」在完成上次提到的任務後,中午下樓到公司附近覓食時,遇到了一隻橘色虎斑貓「阿橘」,然而阿橘正面有難色地看著眼前的機台一直噴出好多貓食乾乾⋯⋯柯基決定上前瞭解狀況。
「你好我是柯基,怎麼這台機器一直噴出乾乾啊!?」柯基驚訝地看著眼前的機台。
「我是乾乾銀行的開發人員,眼前這台是『自動乾乾提存機』」阿橘嘆了口氣說「這台機器主要是供應平常貓貓們在日常被飼主餵太多乾乾,但又不想一次吃完的時候可以把乾乾存到這裡面來。」
「可是⋯⋯」阿橘話還沒說完,機器又噴出更多乾乾把阿橘埋起來了。「⋯⋯這台機器程式不知道為什麼一直有問題,所以我們打算重新來處理他。」
「當然,如果你協助我們的話就給你一年份的乾乾」阿橘邊說邊咬了一口嘴邊的乾乾,然後遞了一張規格書給柯基。柯基直接原地傻眼了幾秒後緩緩說道。
「那⋯⋯乾乾可以換成狗狗可食用的嗎?」
題目
自動乾乾提存機軟體控制主要是由一個 FoodBank
類別在負責處理,其中核心的功能主要有「開戶」、「存款」和「提款」:
而規格對上述程式功能介面上有一些限制要求:
- 資料規格
- 銀行紀錄格式為:
1 2 3 4 5
| safe: { name: { food: <number> } }
|
name
:開戶者名稱,預設資料類型為 string
。
food
:「乾乾」單位,預設資料類型為 number
。
- 開戶
- 需傳入
name
,並檢查是否開過戶頭,若有開過戶頭則回應 您已開過戶頭囉
。
- 交易成功,在 Safe 物件中建立用戶資料。
- 開戶完成後該方法要回應
開戶完成
。
- 存款
- 需傳入
name
,若查詢不到戶頭則交易失敗,該方法要回應 查詢不到該用戶,請重新確認。
- 交易成功,將傳入
food
存入 Safe 物件中
- 交易完成後該方法要回應
存款完成,戶頭目前餘額 {該用戶乾乾數量}
- 提款
- 需傳入
name
,若查詢不到戶頭則交易失敗,該方法要回應 查詢不到該用戶,請重新確認。
- 若傳入的
food
要求超過該用戶戶頭則交易失敗,該方法要回應 餘額不足,你帳戶目前餘額為 {該用戶乾乾數量}
- 交易成功,從 Safe 物件中扣除該用戶的
food
- 交易完成後該方法要回應
提款完成,戶頭目前餘額 {該用戶乾乾數量}
看到這裡,若有想法的讀者可以先按照自己的方式來寫測試哩,若沒想法或遇到困難時也可以參考底下 demo 的部分。
本文可利用系列文專用專案來一邊學習,幫你準備好測試所需要的環境,快來安裝吧!
demo
撰寫測試脈絡
依據之前測試脈絡章節提到的部分,規劃測試時要考量到有以下內容。
- 決定測試類型工具:本系列文主要專注在 Vitest 的單元測試
- 決定測試情境與測試案例
- 思考測試案例路徑
- 撰寫測試案例 3A
決定情境與測試案例
使用系列文專用檔案的讀者 demo 用的檔案位置如下:
測試程式碼 /src/practice/practice-04.spec.js
產品程式碼 /src/practice/practice-04.js
首先建立好測試程式碼並準備好產品程式碼檔案與基本介面:
1 2 3
| import { describe, it} from 'vitest' import { FoodBank } from './FoodBank.js'
|
1 2 3 4 5 6 7 8
| export class FoodBank { constructor() { this.safe = {} } openAccount(name){} deposit(name, food){} withdraw(name, food){} }
|
與接下來開始撰寫測試碼情境,這邊主要依據 FoodBank 提供的方法來拆分:
1 2 3 4 5 6 7 8 9
| describe('openAccount()', () => { }) describe('deposit()', () => { }) describe('withdraw()', () => { })
|
再來依據規格與設計思考要測試案例與案例路徑的部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| describe('開戶', () => { it(`開戶完成,Safe 物件中應有用戶資訊' `, () => {}) it(`開戶完成,應該回應 '開戶完成' `, () => {}) it(`若開過戶頭,應該回應 '您已開過戶頭囉。' `, () => {}) }) describe('存款', () => { it(`存入 100 單位,Safe 物件中該用戶的乾乾應為 100 單位`, () => {}) it(`交易完成,應該回應 '存款完成,戶頭目前餘額 {該用戶乾乾數量}' `, () => {}) it(`若查詢不到戶頭,應該回應 '查詢不到該用戶,請重新確認。' `, () => {}) }) describe('提款', () => { it(`提款 100 單位,Safe 物件中該用戶的乾乾應減少 100 單位`, () => {}) it(`交易完成,應該回應 '存款完成,戶頭目前餘額 {該用戶乾乾數量}' `, () => {}) it(`若查詢不到戶頭,應該回應 '查詢不到該用戶,請重新確認。' `, () => {}) it(`餘額不足,應該回應 '餘額不足,你帳戶目前餘額為 {該用戶乾乾數量}' `, () => {}) })
|
接著撰寫測試案例 3A 部分。
開戶情境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| describe('開戶', () => { it(`開戶完成,Safe 物件中應有用戶資訊' `, () => { const bank = new FoodBank() bank.openAccount('Orange') expect(bank.safe).toEqual({ Orange: { food: 0 } }) }) it(`開戶完成,應該回應 '開戶完成。' `, () => { const bank = new FoodBank() expect(bank.openAccount('Orange')).toBe('開戶完成。') }) it(`若開過戶頭,應該回應 '您已開過戶頭囉。' `, () => { const bank = new FoodBank() bank.openAccount('Orange') expect(bank.openAccount('Orange')).toBe('您已開過戶頭囉。') }) })
|
存款情境:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| describe('存款', () => { it(`存入 100 單位,Safe 物件中該用戶的乾乾應為 100 單位`, () => { const bank = new FoodBank() const user = 'Orange' bank.openAccount(user) bank.deposit(user, 100) expect(bank.safe[user]).toEqual({ food: 100 }) }) it(`交易完成,應該回應 '存款完成,戶頭目前餘額 {該用戶乾乾數量}' `, () => { const bank = new FoodBank() const user = 'Orange' const food = 100 bank.openAccount(user) expect(bank.deposit(user, food)).toEqual(`存款完成,戶頭目前餘額 ${food}`) }) it(`若查詢不到戶頭,應該回應 '查詢不到該用戶,請重新確認。' `, () => { const bank = new FoodBank() const user = 'Orange' const food = 100
expect(bank.deposit(user, food)).toEqual(`查詢不到該用戶,請重新確認。`) }) })
|
提款部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| describe('提款', () => { it(`提款 100 單位,Safe 物件中該用戶的乾乾應減少 100 單位`, () => { const bank = new FoodBank() const user = 'Orange' bank.openAccount(user) bank.deposit(user, 100) bank.withdraw(user, 100) expect(bank.safe[user]).toEqual({ food: 0 }) }) it(`交易完成,應該回應 '存款完成,戶頭目前餘額 {該用戶乾乾數量}' `, () => { const bank = new FoodBank() const user = 'Orange' bank.openAccount(user) bank.deposit(user, 100)
expect(bank.withdraw(user, 100)).toBe('存款完成,戶頭目前餘額 0') }) it(`若查詢不到戶頭,應該回應 '查詢不到該用戶,請重新確認。' `, () => { const bank = new FoodBank() const user = 'Orange'
expect(bank.withdraw(user, 100)).toBe('查詢不到該用戶,請重新確認。') }) it(`餘額不足,應該回應 '餘額不足,你帳戶目前餘額為 {該用戶乾乾數量}' `, () => { const bank = new FoodBank() const user = 'Orange' bank.openAccount(user)
expect(bank.withdraw(user, 100)).toBe('餘額不足,你帳戶目前餘額為 0') }) })
|
接著執行測試,確認測試案例應該是錯誤之後,接著利用 only
將測試案例鎖定在各個情境下,一邊開發產品程式碼,這樣就不會受到其他測試情境底下的案例錯誤干擾開發過程:
1 2 3 4 5 6 7 8 9
| describe.only('開戶', () => { }) describe('存款', () => { }) describe('提款', () => { })
|
最後按著平常開發流程去補齊產品程式碼,完成一部分的情境就透過 .only
, .skip
等輔助 API,來觀察測試結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| export class FoodBank { constructor() { this.safe = {} }
openAccount(name) { if (this.safe[name]) return '您已開過戶頭囉。' this.safe[name] = { food: 0 } return `開戶完成。` }
deposit(name, food) { if (!this.safe[name]) return '查詢不到該用戶,請重新確認。' this.safe[name].food += food return `存款完成,戶頭目前餘額 ${this.safe[name].food}` }
withdraw(name, food) { if (!this.safe[name]) return '查詢不到該用戶,請重新確認。'
if (this.safe[name].food >= food) { this.safe[name].food -= food return `存款完成,戶頭目前餘額 ${this.safe[name].food}` } else { return `餘額不足,你帳戶目前餘額為 ${this.safe[name].food}` } } }
|
此時一邊開發的過程應該會看到錯誤的部分逐漸陸續通過,直到最後將所有 .only
, .skip
等輔助 API 拔除後依然都是通過的情況,就能確保你的開發既符合測試,後續測試也將繼續保護你的產品程式碼!
而這個開發過程也就是所謂的「測試驅動開發(TDD, Test-driven development)」中的「紅燈開發(Red-Green-Refactor)」,很有趣吧!
後續進階章節將會補上有關於 「測試驅動開發(TDD, Test-driven development)」更為詳細的介紹,而明天我們將來繼續學習其餘語法的部分,讓我們能夠測試更多的東西!
題外話:今天文章寫到最後時已經大概十一點多了,電腦突然一個卡死,差點嚇爆⋯⋯