Logo

新人日誌

首頁關於我部落格

新人日誌

Logo

網站會不定期發佈技術筆記、職場心得相關的內容,歡迎關注本站!

網站
首頁關於我部落格
部落格
分類系列文

© 新人日誌. All rights reserved. 2020-present.

單元測試到底在測什麼?從你每天都在做的事開始講起

最後更新:2026年8月3日基礎概念

你有沒有遇過這種狀況:改了一行程式碼,結果三天後某個完全沒碰過的功能壞掉了?

或是每次要交付前,都得手動把整個流程再點一遍,深怕漏掉什麼?

還有,大家都說要寫單元測試,但「單元」到底是多大一塊?一個函式?一個檔案?還是一整個功能?

手動測試跟自動化測試差在哪?

講到測試,你可能會想:我每次改完程式都會自己跑一次看看,這不算測試嗎?

算,那叫手動測試,而且它是每個人的起點。

打開瀏覽器點一點、在 console 印個值出來看、丟幾筆假資料進去試試看,這些都是在測試,只是執行的人是你,判斷對錯的也是你。

問題從來不是手動測試不對,而是它撐不到專案長大。

這件事用一個例子最好講,下面這個函式,這篇文章之後都會拿它當例子:

function calculateDiscount(price, memberLevel) {
  if (memberLevel === 'gold') return price * 0.8
  if (memberLevel === 'silver') return price * 0.9
  return price
}

它做的事情很單純:看會員等級決定打幾折。

手動測試的三個先天限制

現在假設這個函式在專案裡被 30 個地方用到,而你今天改了它其中一行。

這時候你要怎麼確定自己沒有改壞任何東西?

第一,它會累

理論上這 30 個地方你都該重新點過一次,因為你沒辦法確定哪些頁面會受影響。

實際上你會點幾個?大概是你「覺得有關」的那三四個。

剩下的二十幾個就是靠運氣,而 bug 最愛躲的就是那二十幾個裡面。

第二,它會忘

三個月前你修過一個 bug:後端回傳的會員等級有時候是大寫的 'Gold',字串比對不到,金卡會員被收了原價。

你在函式裡加上 memberLevel.toLowerCase() 修好它,然後手動試了 'Gold'、'GOLD'、'gold',確認三種都會打八折。

三個月後,另一個人來重構這段程式,他把等級比對改寫成查一張對照表,順手把那行 .toLowerCase() 拿掉了。

他有測嗎?有,他點開畫面上的金卡會員,看到八折,覺得沒問題。

因為那筆測試資料剛好是小寫的。

大寫那條路從此沒有人再走過一次,於是同一個 bug 又回到了正式環境。

這種「修好之後又壞掉」的狀況有個名字,叫回歸(regression),它正是自動化測試最主要想擋住的東西。

第三,它不留下紀錄

回想一下你剛剛修大小寫那個 bug 的時候,腦袋裡其實有一份清單:'Gold' 要打折、'GOLD' 要打折、原本的 'gold' 不能壞掉。

你照著這份清單一個一個試過,確認都對,然後關掉 console。

那份清單就這樣消失了。

專案裡不會有任何一個地方寫著「calculateDiscount 該滿足這三件事」,程式碼只寫了它現在怎麼算,沒寫哪些是當初刻意訂下的規則。

所以三個月後接手的人看到 .toLowerCase() 那一行,分不出這是為了修 bug 特地加的,還是誰順手寫的防禦性程式碼。

他只能猜,然後帶著不確定改下去。

如果那份清單當初是寫成測試的,這裡就不用猜了。

他會在測試檔案裡看到一個叫「大寫的 Gold 也要打八折」的測試,那就是答案:這一行是刻意的,別動。

從 console.log 到一個測試

自動化測試就是把「我剛剛用手做的檢查」寫成程式碼,讓電腦幫你重複做。

它的門檻其實比你想的低很多,因為你早就寫過很像的東西了:

// 手動測試:你在 console 裡打這一行,然後用眼睛看結果對不對
console.log(calculateDiscount(100, 'gold'))  // 你期待看到 80

這行 console.log 已經很接近一個測試了,它只缺了一塊:判斷。

程式印出 80,但「80 是對的」這個結論是你在腦袋裡下的,程式本身完全不知道。

把這個判斷也交給程式,它就變成自動化測試了:

// 自動化測試:讓程式自己判斷對不對
test('gold member gets 20% off', () => {
  expect(calculateDiscount(100, 'gold')).toBe(80)
})

這幾行你現在還不用會寫,先看懂它在說什麼就好:

  • test('...') 開一個測試,引號裡那句話是它的名字,用來說明這個測試在檢查什麼
  • expect(...) 包住你要檢查的東西,這裡包的是 calculateDiscount(100, 'gold') 的回傳值
  • .toBe(80) 是你的預期,意思是「它應該等於 80」,不是的話這個測試就失敗

連起來唸其實就是一句英文:expect calculateDiscount(100, ‘gold’) to be 80。

至於 test 和 expect 是哪裡來的、為什麼可以直接用,後面講測試框架的時候會回頭交代。

這兩段做的事情其實一樣,都是「呼叫函式、檢查結果」。

差別在三個地方。

判斷的人不同

上面那段要你用眼睛看,下面那段由 expect 自己比對,結果不對就直接報錯。

成本不同

上面那段每測一次就要花掉你一次注意力,下面那段寫完之後,跑一百次的成本跟跑一次幾乎一樣。

壽命不同

console.log 通常在你確認完就順手刪掉了,測試則會留在專案裡,變成之後每一次修改的安全網。

自動化取代的是「重複」,不是「人」

不過有件事要講清楚:自動化測試不是要取代手動測試。

像「這個畫面好不好看」「這段流程順不順」這種問題,人的眼睛還是可靠得多。

自動化真正取代的是重複的那一部分,也就是那些你已經確認過一次、但之後每次改動都得再確認一遍的檢查。

換句話說,自動化測試不是什麼新技術,只是把你本來用眼睛做的檢查交給程式碼去做而已。

不過「自動化測試」是個很大的詞,它底下有好幾種,差別在於你一次測多大一塊。

由大到小,大致可以分成三層:

  • E2E 測試(end-to-end,端對端測試):模擬一個真實使用者,開瀏覽器、點按鈕、填表單、送出訂單,從頭到尾把整個流程跑完一遍
  • 整合測試(integration test):測幾塊程式串起來會不會動,例如結帳流程去呼叫折扣計算、庫存檢查、金流串接,這幾塊接得對不對
  • 單元測試(unit test):一次只測一塊獨立的邏輯,例如 calculateDiscount 自己算得對不對,不管誰呼叫它,也不管畫面長什麼樣

範圍越大,越接近使用者真正會遇到的情況,但也跑得越慢、越容易因為無關的小改動就失敗。

範圍越小則相反:跑得飛快、壞掉時一眼就知道是哪裡壞的,但它保證不了「這些零件湊起來真的能用」。

所以這三種不是互相取代的關係,實務上通常三種都會有,只是範圍越大的寫得越少——單元測試數量最多,E2E 最少。

這個分布畫出來像個三角形,所以常被叫做測試金字塔(testing pyramid)。

而這篇文章要講的,是最底下也最多的那一層:單元測試。

那「單元」到底是多大一塊?

「單元」是什麼?

單元測試(unit test)裡的「單元」,指的是程式中可以獨立被呼叫、而且有明確輸入輸出的最小一塊邏輯。

在大多數情況下,這一塊就是一個函式(function)或一個 class 的一個方法(method)。

白話講,如果你可以指著一段程式碼說「給它這些輸入,它應該吐出那個結果」,那它就是一個單元。

剛剛那個 calculateDiscount 正好就是一個很典型的單元。

它有清楚的輸入(price 和 memberLevel),有清楚的輸出(打折後的價格),而且不需要資料庫、不需要網路、不需要任何外部東西就能執行。

這也是為什麼我們可以用一行 expect 就把它測完:你只要餵資料進去、比對結果就好。

反過來說,如果一個函式又要打 API、又要讀設定檔、又要改全域變數,它就不是一塊乾淨的單元,測起來也會相對痛苦,這件事我們後面會再回頭談。

所以「單元」沒有想像中那麼玄,它就是一個你能單獨呼叫、而且知道它該回傳什麼的函式。

一個測試長什麼樣子:Arrange-Act-Assert

幾乎所有單元測試都可以拆成三個階段,業界習慣叫它 AAA。

Arrange(準備)

把測試需要的資料、物件準備好。

Act(執行)

呼叫你要測的那個單元。

Assert(驗證)

檢查結果是不是你預期的。

把剛才的例子按這三段拆開:

test('gold member gets 20% off', () => {
  // Arrange:準備輸入資料
  const price = 100
  const memberLevel = 'gold'

  // Act:執行要測試的單元
  const result = calculateDiscount(price, memberLevel)

  // Assert:驗證結果
  expect(result).toBe(80)
})

這三行註解你不一定要真的寫在程式裡,但腦袋裡最好有這個結構。

當你不知道測試該怎麼開始寫的時候,就問自己這三個問題:我需要什麼資料?我要呼叫哪個函式?我期待什麼結果?

而這三步裡面,真正決定一個測試有沒有用的是最後那個 Assert,我們接著看它。

Assertion:測試的判斷句

剛才三步裡的 Assert,也就是 expect(result).toBe(80) 那一行,正式名稱叫 assertion(斷言)。

它是整個測試裡唯一會下判斷的地方:前面兩步做得再漂亮,只要少了這一行,這個測試就等於沒測。

這不是誇飾,是真的會過。

沒有 assertion 的測試

// ❌ 這個測試永遠會通過,因為它沒有做任何判斷
test('calculateDiscount works', () => {
  calculateDiscount(100, 'gold')
})

這段程式碼只是呼叫了函式,沒有檢查回傳值。

只要函式不爆炸,測試就會過,就算它回傳 undefined 也一樣。

有 assertion 的測試

// ✅ 有 assertion,結果不對就會失敗
test('gold member gets 20% off', () => {
  expect(calculateDiscount(100, 'gold')).toBe(80)
})

這段明確宣告了「結果必須等於 80」。

如果哪天有人改壞折扣邏輯,這個測試就會紅燈,你馬上知道。

常見的 assertion

除了 toBe,還有幾種你很快就會用到:

  • toBe / toEqual:檢查值相不相等,前者比對純值,後者會逐層比對物件內容
  • toThrow:檢查有沒有丟出錯誤
  • toHaveBeenCalled:檢查某個函式有沒有被呼叫過
  • toBeNull / toBeUndefined:檢查是不是特定的空值

不同框架的名字會不太一樣,但概念是一樣的。

所以之後在看別人寫的測試時,可以先找 assertion 那一行,那才是這個測試真正在乎的事情。

測試框架幫你做了什麼?

前面我們一直在用 test() 和 expect(),但一直沒說它們是哪裡來的。

這些是測試框架提供的。

JavaScript 圈常見的有 Jest、Vitest、Mocha。

這篇文章的範例用的是 Jest 和 Vitest 的語法。

這兩套的 API 幾乎一模一樣,test、expect、describe 在兩邊都能直接用,換過去不太需要改寫。

框架主要幫你做三件事。

test():把一段程式碼圈成一個測試

前面那個什麼都沒檢查的例子,你可能會想:既然它不做判斷,那 test() 到底有什麼用?

test() 負責的不是判斷,而是另外三件事:圈範圍、取名字、隔離失敗。

圈範圍的意思是,大括號裡那段程式碼會被當成一個獨立的單位執行,框架知道它從哪開始、到哪結束。

取名字就是第一個參數那個字串,測試失敗時印出來的就是它。

隔離失敗是最容易被忽略、但最關鍵的一個:如果沒有 test() 包著,一個檔案裡第一段程式碼丟出錯誤,整個檔案就停在那裡,後面的檢查全部不會執行。

有 test() 包著,框架會把那個錯誤記成「這個測試失敗」,然後繼續跑下一個測試。

所以你最後拿到的是「10 個測試,3 個失敗,這 3 個分別是什麼」,而不是「程式在第 12 行爆炸了」。

回到那個沒有 assertion 的例子。

test() 確實有把它圈起來、有跑、也有回報結果,該做的事一件都沒少。

只是裡面沒有任何會讓它失敗的東西,所以它回報的永遠是通過。

順帶一提,test() 和 it() 是同一個東西的兩個名字,it 是為了讓句子讀起來像英文(it('returns 80 for gold members')),選你喜歡的就好。

expect():幫你寫判斷跟失敗訊息

沒有框架的話,一個 assertion 得自己這樣寫:

const result = calculateDiscount(100, 'gold')
if (result !== 80) {
  throw new Error(`預期 80,實際拿到 ${result}`)
}

expect(result).toBe(80) 做的就是這件事,但你不用每次都手寫比較、拋錯、還有那句錯誤訊息。

而且框架印出來的失敗訊息會比你手寫的完整得多,它會告訴你預期什麼、實際拿到什麼、差在哪裡,物件的話還會標出是哪個欄位對不上。

test runner:把測試全部找出來跑一遍

你不會想要每個測試檔案都自己 node 執行一次。

Test runner 會依照命名規則(通常是 *.test.js 或 *.spec.js)自動掃出專案裡所有的測試檔案,一次跑完,然後彙整成一份報告:總共幾個、過了幾個、failed 的是哪幾個、各自壞在哪一行。

它通常還有 watch 模式,你存檔的當下就自動重跑相關的測試——這正是前面說「快到你願意每次存檔都跑一次」能成立的原因。

用 describe 把測試分組

describe 一樣是 Jest / Vitest(還有 Mocha)都有的函式,它讓你把相關的測試收在同一組底下:

describe('calculateDiscount', () => {
  test('gold member gets 20% off', () => {
    expect(calculateDiscount(100, 'gold')).toBe(80)
  })

  test('silver member gets 10% off', () => {
    expect(calculateDiscount(100, 'silver')).toBe(90)
  })

  test('normal member pays full price', () => {
    expect(calculateDiscount(100, 'normal')).toBe(100)
  })
})

describe 把同一個對象的測試收在一起,執行結果印出來的時候比較好讀。

裡面每個 test 各自獨立,一個失敗不會影響其他的執行。

測試的名字就是它的文件

好的測試名稱是一句完整的話,描述「在什麼情況下應該發生什麼」。

當測試失敗時,你光看名字就該知道壞掉的是哪個行為,不用點進去看程式碼。

說穿了,框架只負責跑測試跟印結果,「什麼情況下應該得到什麼」還是得你自己想清楚。

什麼是好的單元測試?

會寫測試之後,下一個問題是:怎樣算寫得好?

有幾個特質是公認的。

獨立(Independent)

每個測試都要能單獨執行,而且順序打亂也要能過。

互相依賴的寫法

// ❌ 測試之間互相依賴
let cart = []

test('add item to cart', () => {
  cart.push({ id: 1, price: 100 })
  expect(cart.length).toBe(1)
})

test('calculate total', () => {
  expect(getTotal(cart)).toBe(100)  // 依賴上一個測試留下的 cart
})

問題出在最上面那個 cart。

它宣告在兩個測試的外面,所以是兩個測試共用的同一份資料。

第一個測試往裡面 push 了一筆 100 元的商品,第二個測試則預期總金額算出來是 100——但第二個測試自己從頭到尾沒有準備過任何商品。

它拿去計算的那筆資料,其實是第一個測試跑完之後留在 cart 裡的殘留物。

也就是說,第二個測試能不能過,取決於第一個測試有沒有先跑、而且有沒有成功。

如果你只想單獨跑第二個測試,它會失敗;如果有人調換順序,它也會失敗。

各自準備資料的寫法

// ✅ 每個測試自己準備資料
test('add item to cart', () => {
  const cart = []
  addItem(cart, { id: 1, price: 100 })
  expect(cart.length).toBe(1)
})

test('calculate total', () => {
  const cart = [{ id: 1, price: 100 }]
  expect(getTotal(cart)).toBe(100)
})

現在兩個測試各自準備自己需要的資料,誰先跑誰後跑都無所謂。

如果多個測試需要同樣的初始化,可以用框架提供的 beforeEach,它會在每個測試前重新執行一次。

可重複(Repeatable)

同樣的測試跑一百次,結果要一樣。

會破壞這件事的通常是三種東西:目前時間、亂數、外部服務。

直接讀取當下時間

// ❌ 這個測試明天就會壞
test('should be today', () => {
  expect(formatDate(new Date())).toBe('2026-08-02')
})

new Date() 每天回傳不同的值,今天過的測試明天就紅了。

把時間變成參數

// ✅ 把不確定的東西變成輸入
test('formats date correctly', () => {
  const fixedDate = new Date('2026-08-02')
  expect(formatDate(fixedDate)).toBe('2026-08-02')
})

把時間當成參數傳進去,函式的行為就變成完全可預測的。

快(Fast)

單元測試應該在毫秒等級跑完。

因為只有夠快,你才會願意每次存檔都跑一次;跑一次要等三分鐘的測試,大家最後都會關掉。

要夠快,就代表測試裡不該有真的資料庫查詢、真的 API 呼叫、真的檔案讀寫。

獨立、可重複、快,看起來是三個要求,但你會發現它們常常是同一個問題的三個面向:只要測試碰到外面的世界,這三件事就會一起壞掉。

那如果我的函式就是需要這些東西呢?這就要講到下一段了。

遇到相依性怎麼辦?Test Double

先看看什麼叫「有相依性的程式碼」:

async function getUserGreeting(userId) {
  const user = await fetchUser(userId)   // 打 API
  return `Hello, ${user.name}!`
}

這個函式做的事情很單純,但它中間呼叫了 fetchUser,而 fetchUser 會真的發出網路請求。

如果直接測它,會有一堆問題:網路斷線測試就失敗、API 回傳的資料變了測試也失敗、而且每次跑都要等好幾百毫秒。

重點是,你現在想測的是「字串有沒有正確組出來」,不是「API 通不通」。

解法是用 test double:做一個假的替身,取代真正的相依對象。

Stub、Mock、Spy 差在哪?

Test double 有幾種常見類型:

  • Stub:回傳你指定的固定資料,用來控制輸入
  • Mock:除了回傳資料,還會記錄「有沒有被呼叫、被呼叫幾次、參數是什麼」
  • Spy:包在真實實作外面,讓真的邏輯照跑,但同時記錄呼叫過程

實務上大家的用詞常常混著用,你不用太糾結名詞,重點是知道「我到底是要控制回傳值,還是要驗證呼叫行為」。

用 stub 改寫這個測試

把 fetchUser 換成一個寫死回傳值的假函式,測試就跟網路無關了:

test('greets user by name', async () => {
  // Arrange:用假的 fetchUser 取代真的
  const fakeFetchUser = async () => ({ id: 1, name: 'Amy' })

  // Act
  const result = await getUserGreeting(1, fakeFetchUser)

  // Assert
  expect(result).toBe('Hello, Amy!')
})

現在測試不碰網路了,執行時間是毫秒級,而且結果完全可預測。

把外部相依換成可控的替身,你才能專心測自己寫的那段邏輯,而不是順便幫別人家的服務做健康檢查。

不過你可能發現了:上面的 getUserGreeting 多了第二個參數。

這不是巧合,這帶到單元測試最重要的一個副作用。

難測的程式碼,通常是設計有問題

相依寫死在函式裡

原本的版本裡,fetchUser 是寫死在函式內部的:

// ❌ 相依寫死在裡面,外面無法替換
import { fetchUser } from './api'

async function getUserGreeting(userId) {
  const user = await fetchUser(userId)
  return `Hello, ${user.name}!`
}

問題在於 fetchUser 是從模組外面 import 進來的,函式內部直接抓來就用。

它沒有出現在參數列裡,所以你在外面呼叫 getUserGreeting(1) 的時候,除了 userId 之外沒有別的東西可以傳。

想指定「這次請你用另一個 fetchUser」,是做不到的,因為根本沒有地方可以指定。

不管是正式環境還是測試裡,跑的都是同一個會真的發出網路請求的 fetchUser。

那還是想測,怎麼辦?

框架有提供一個辦法,叫 module mock,在測試檔案最上面加一行:

jest.mock('./api')

這行的意思是「在這個測試檔案裡,凡是從 ./api 拿到的東西,通通換成假的」。

它確實有效,但你應該感覺得到哪裡不太對勁:為了測一個只是把名字組成字串的函式,你得先去干預整個模組的載入過程。

而且這個寫法認的是檔案路徑。

哪天有人把 api.js 搬到別的資料夾,或是把它拆成兩個檔案,getUserGreeting 的行為一點都沒變,你的測試卻會跟著壞掉。

把相依從外面傳進來

// ✅ 把相依從參數傳進來
async function getUserGreeting(userId, fetchUser) {
  const user = await fetchUser(userId)
  return `Hello, ${user.name}!`
}

現在誰要呼叫它,誰就決定要傳哪個 fetchUser 進去。

正式環境傳真的,測試時傳假的,函式本身完全不用改。

這個做法叫依賴注入(dependency injection),意思就是「不要自己去拿相依的東西,讓外面把它給你」。

這裡有個很值得記住的觀察:當你覺得某段程式碼很難寫測試的時候,通常不是測試技巧不夠,而是那段程式碼把太多責任綁在一起了。

「難測」是一個訊號,它在告訴你這段程式碼的耦合太高。

所以下次測試寫不動的時候,先別急著找更厲害的 mock 工具,回頭看看那段程式碼的設計。

測試涵蓋率是什麼?能相信嗎?

測試涵蓋率(code coverage)是一個數字,表示你的測試執行過原始碼的多少比例。

框架通常會分幾種算法,最常看的是行涵蓋率(line coverage)跟分支涵蓋率(branch coverage)。

行涵蓋率算的是「有多少行程式碼被跑過」,分支涵蓋率算的是「每個 if-else 的每條路徑有沒有都走過」。

這個數字有用,但它只能告訴你一件事:哪些程式碼完全沒被測到。

它不能告訴你測試寫得好不好。

看這個例子:

test('runs without error', () => {
  calculateDiscount(100, 'gold')
  calculateDiscount(100, 'silver')
  calculateDiscount(100, 'normal')
})

這個測試把 calculateDiscount 的每一行都跑過了,涵蓋率 100%。

但它沒有任何 assertion,就算有人把八折寫成八倍,它也照樣通過。

所以涵蓋率適合拿來找「完全沒人測過的角落」,不適合拿來當 KPI。

一個 60% 涵蓋率但每個測試都認真驗證的專案,比 95% 涵蓋率但都在空跑的專案可靠得多。

把涵蓋率當成照明燈就好,它負責照出你還沒測到的角落,但它不是成績單。

什麼時候該寫單元測試?

不是所有程式碼都值得寫單元測試,這件事要說清楚。

該測的跟不該測的

很值得寫的

  • 計算邏輯,例如金額、折扣、稅率、日期換算
  • 資料轉換,例如把 API 回傳格式轉成畫面要的格式
  • 條件判斷多的地方,例如各種權限規則、狀態機
  • 修過的 bug,把觸發 bug 的情境寫成測試,確保它不會再回來

這些的共同點是:輸入輸出明確、邏輯有分支、而且改壞了不容易一眼看出來。

不太適合用單元測試的

  • 純粹的 UI 排版,比較適合用視覺測試或人眼看
  • 只是把參數轉手傳給別人的薄薄一層函式,測了等於在測語言本身
  • 「多個模組串起來會不會動」,這是前面提過的整合測試的守備範圍,不該用單元測試硬做

測行為,不要測實作

這裡也要講清楚取捨:測試不是免費的。

測試是程式碼,程式碼就要維護。

當你需求一改,對應的測試也要跟著改;如果測試寫得太貼著實作細節,重構時會變成沉重的負擔。

一個實用的判準是:測試應該綁在「行為」上,不要綁在「實作方式」上。

綁在實作上的測試

// ❌ 綁在實作細節上,把內部改成 map 就壞了
test('uses forEach to sum', () => {
  const spy = jest.spyOn(Array.prototype, 'forEach')
  sumPrices([10, 20])
  expect(spy).toHaveBeenCalled()
})

這個測試在檢查「你有沒有用 forEach」,但那是實作選擇,不是需求。

你把 forEach 改成 reduce,行為完全一樣,測試卻紅了。

綁在行為上的測試

// ✅ 綁在行為上,內部怎麼寫都無所謂
test('sums all prices', () => {
  expect(sumPrices([10, 20])).toBe(30)
})

這個測試只關心「加起來對不對」。

不管內部改用哪種寫法,只要結果正確,測試就會一直是綠的——這才是重構時真正保護你的東西。

這也是決定要不要寫、該怎麼寫的最後一把尺:挑改壞了不容易發現的地方測,而且永遠測行為,不要測實作。

最後:從一個函式開始

回到開頭那三個問題。

改一行程式碼會不會弄壞別的地方、交付前要不要把整個流程再點一遍、「單元」到底是多大一塊——這三件事現在應該都有答案了。

不過知道跟做得到之間還有一段距離,所以最後給你一個起點。

不用從整個專案開始,挑一個函式就好,最好是那種輸入輸出明確、但裡面有點分支的:算折扣、轉資料格式、判斷權限都很適合。

幫它寫三個測試:一個正常情況、一個邊界情況,還有一個你上次真的踩到的 bug。

寫的時候守住三件事就好。

第一,每個測試都要有 assertion,不然它只是在執行程式碼,不是在測試。

第二,如果寫到一半覺得綁手綁腳、非得先 mock 掉一堆東西才動得了,那通常不是你不會寫測試,是那個函式的耦合在跟你抗議。

第三,測它做出什麼,不要測它怎麼做,這樣之後重構的時候,測試才會站在你這邊。

寫完那三個測試,你就有了一份不會消失的清單。

目前還沒有留言,成為第一個留言的人吧!

發表留言

留言將在審核後顯示。

基礎概念

目錄

  • 手動測試跟自動化測試差在哪?
  • 手動測試的三個先天限制
  • 從 console.log 到一個測試
  • 自動化取代的是「重複」,不是「人」
  • 「單元」是什麼?
  • 一個測試長什麼樣子:Arrange-Act-Assert
  • Arrange(準備)
  • Act(執行)
  • Assert(驗證)
  • Assertion:測試的判斷句
  • 沒有 assertion 的測試
  • 有 assertion 的測試
  • 常見的 assertion
  • 測試框架幫你做了什麼?
  • test():把一段程式碼圈成一個測試
  • expect():幫你寫判斷跟失敗訊息
  • test runner:把測試全部找出來跑一遍
  • 用 describe 把測試分組
  • 測試的名字就是它的文件
  • 什麼是好的單元測試?
  • 獨立(Independent)
  • 可重複(Repeatable)
  • 快(Fast)
  • 遇到相依性怎麼辦?Test Double
  • Stub、Mock、Spy 差在哪?
  • 用 stub 改寫這個測試
  • 難測的程式碼,通常是設計有問題
  • 相依寫死在函式裡
  • 把相依從外面傳進來
  • 測試涵蓋率是什麼?能相信嗎?
  • 什麼時候該寫單元測試?
  • 該測的跟不該測的
  • 測行為,不要測實作
  • 最後:從一個函式開始