← JavaScript Learning Path

JAVASCRIPT V2 · ENGINEERING · 78

Testing Basics:把「我試過可以」變成可重複的 Contract Evidence

手動點一次畫面只能證明「剛才那一次看起來可以」。Automated test 的價值是把 input、behavior、expected outcome 寫成可重複執行的 contract,讓 refactor、bug fix 與未來修改都有 regression evidence。

Learning outcomes

1. Why now:程式會改,記憶不會自動驗證

function isPassed(score) {
  return score >= 60;
}

今天你手動輸入 80 得到 true。明天有人把規則改成 score > 60,80 仍然通過,但 60 已經被破壞。如果沒有 boundary test,這個 regression 很容易進 production。

2. Mental model:Test 是可執行的 Contract Example

Given:
score = 60

When:
isPassed(score)

Then:
result === true

Test 不只是「找 bug」。它同時記錄這個 behavior 對未來維護者意味著什麼。

3. Worked Example A:Arrange / Act / Assert

test(
  "60 is passing",
  () => {
    // Arrange
    const score = 60;

    // Act
    const actual =
      isPassed(score);

    // Assert
    expectEqual(
      actual,
      true
    );
  }
);

AAA 是閱讀結構,不是宗教規範。目的在讓 failure 時能快速分辨 setup、action、expectation。

4. Boundary Analysis:59 / 60 / 61 比 80 更有資訊

59 → false
60 → true
61 → true

Requirement 出現 >=、range、length、limit、empty/non-empty 時,boundary 附近通常最值得測。

5. Worked Example B:Validation Matrix

normalizeScore("0")   → 0
normalizeScore("60")  → 60
normalizeScore("100") → 100

normalizeScore("-1")  → error
normalizeScore("101") → error
normalizeScore("abc") → error
normalizeScore("")    → error

一個 behavior contract 往往不是一個 test,而是一組 partition:valid、boundary、invalid、empty。

6. Deterministic:同條件應得到同結果

function greeting() {
  return Date.now()
    + Math.random();
}

如果 test 直接依賴現在時間、random、production network,結果可能因環境波動而改變。Engineering 的做法是控制 boundary,而不是祈禱 CI 當下網路穩定。

7. Isolation:Test 不應互相污染

let students = [];

test("adds one", () => {
  students.push("Amy");
});

test("starts empty", () => {
  // 如果上一題沒 reset,
  // 這題就被污染
});

共享 mutable state 會產生 order-dependent tests。每個 test 應建立自己的 fixture,或在明確 setup/teardown 中重置。

8. Worked Example C:Test Behavior,不綁 Implementation

expectEqual(
  average([80, 100]),
  90
);

這個 test 不在乎 average 用 loop、reduce 還是其他演算法。只要 observable contract 沒變,重構不該逼你重寫測試。

9. Common mistakes

A. 只測 happy path

80 通過無法證明 60 boundary 正確。

B. 一個 test 驗十件事

失敗時不知道哪個 contract 壞掉。

C. Test 依賴 production API

外部 outage 會讓 unit-level signal 失真。

D. 為 implementation detail 寫 assertion

合理 refactor 會造成大量假紅燈。

E. Test 名稱只有「works」

Failure report 無法告訴你哪個 behavior 出錯。

10. Debug evidence:Test 紅不等於 Production Code 一定錯

Failure checklist:
1. actual input?
2. expected contract?
3. actual output/error?
4. requirement changed?
5. shared state?
6. environment dependency?

Test 本身也是 code,也可能 expectation 過時或 setup 錯誤。

11. Guided exercise:Score Boundary Tests

function isPassed(
  score
) {
  return score >= 60;
}

// write tests for:
// 59, 60, 61

要求每個 case 有清楚名稱,failure message 能指出 input 與 expected。

12. Independent exercise:normalizeScore Contract

先寫 cases,再實作 function。至少包含:

"0"
"100"
"60"
"-1"
"101"
"abc"
""
" 90 "

你必須先定義空字串與 whitespace 的 contract,不能讓 JavaScript coercion 偷偷替你決定產品規則。

13. Challenge:Regression First

假設 production bug 是「score=60 被判定 fail」。先寫一個會失敗的 regression test,確認它真的抓到 bug,再修 implementation,最後確認 test 轉綠。這就是 red → green 的最小 bug-fix evidence。

Project checkpoint:Grade Tracker Contract Matrix

normalizeScore
├─ valid
├─ boundary
├─ invalid
└─ empty

isPassed
├─ 59 false
├─ 60 true
└─ 61 true

average
├─ empty
├─ one item
└─ multiple items

從這一課開始,Final Project 的每個純規則都要有 executable tests,不再只靠 UI 點擊。

14. Vocabulary / Summary

15. Further reading

Knowledge check · Mastery

  1. 說明 manual check 與 automated regression 的差異。
  2. 為 0–100 score contract 設計 partitions/boundaries。
  3. 完成頁面下方 Real Test Runner missions。
  4. 說明「test fail」有哪些可能 root causes。