← JavaScript Learning Path

JAVASCRIPT V2 · ENGINEERING · 120

Async Test / Fake / Mock:控制 Boundary,不是把整個世界假掉

當 code 依賴 network、repository、clock、timer 或 cancellation,unit test 若直接碰真實外部世界就容易慢、flaky、難重現。Test double 的目的,是把不穩定 dependency 換成可控制 contract,讓成功、失敗、延遲與取消都能重播。

Learning outcomes

1. Why now:Async Failure 很難靠手動碰巧重現

network success
network 500
network reject
slow response
cancelled request

如果每一種情境都要等真實 server 配合,你的 test suite 不會穩定。工程上要能主動製造 dependency behavior。

2. Mental model:Production Logic 與 Dependency 分開

loadDashboard(repo)
      ↓
business logic

repo.load()
      ↓
production repo
or
fake repo in test

同一 service logic,換不同受控 dependency,就能重現 success/error/empty 等 paths。

3. Worked Example A:Async Test 必須真的 Await

test(
  "loads students",
  async () => {
    const students =
      await loadStudents(
        fakeRepo
      );

    expectEqual(
      students.length,
      2
    );
  }
);

Test runner 必須知道 Promise 尚未 settled;否則 assertion/error 可能在 test 已被標成完成後才發生。

4. Fake Repository:可運作的簡化 Implementation

const fakeRepo = {
  async load() {
    return [
      {
        id: 1,
        name: "Amy"
      }
    ];
  }
};

Fake 不需要模擬真 DB 每個細節,只要實作同一 dependency contract,並保持 deterministic。

5. Worked Example B:故意 Reject

const failingRepo = {
  async load() {
    throw new Error(
      "DB unavailable"
    );
  }
};

await expectRejects(
  () => loadStudents(
    failingRepo
  )
);

Failure path 只有在 test 能主動製造 failure 時才會真正被驗證。

6. Interaction Mock:只有 Interaction 本身是 Contract 才測

const save =
  mockFn();

await submitStudent(
  { name: "Amy" },
  { save }
);

expectEqual(
  save.calls.length,
  1
);

expectEqual(
  save.calls[0][0].name,
  "Amy"
);

如果 contract 是「valid student 必須交給 repository.save」,interaction assertion 很合理。

7. Dependency Injection 不必先有 Framework

async function loadStudents(
  repo
) {
  const rows =
    await repo.load();

  return rows.map(
    normalizeStudent
  );
}

把 dependency 當參數就是最直接的 injection。重點是 ownership 與 contract 清楚,不是一定要 DI container。

8. Worked Example C:Cancellation Path

async function search(
  query,
  request
) {
  return request(query);
}

const cancelled =
  async () => {
    const error =
      new Error("cancelled");

    error.name =
      "AbortError";

    throw error;
  };

你可以用 fake request 精確重現 cancellation,而不必真的開 network 再 race abort timing。

9. Clock / Timer Boundary

function isExpired(
  expiresAt,
  now
) {
  return now >= expiresAt;
}

把現在時間作為 dependency/value 傳入,test 就不需要真的等待或依賴測試機當下時鐘。

10. Common mistakes

A. 忘記 await / return Promise

Test 可能假綠。

B. 所有內部 function 都 mock

最後只證明 mocks 按你的劇本互相配合。

C. Fake 與 production contract 漂移

Integration tests 仍然必要。

D. 只測 success,不測 rejection

真正 incident 往往發生在 error path。

E. Mock call count 取代 behavior assertion

只有 interaction 是需求時才值得。

11. Debug evidence:Mock 全綠,Production 還是壞

Unit:
service + fake repo
PASS

Production:
service + HTTP + auth + DB
FAIL

這不代表 unit test 沒價值;它表示 failure 在未被 unit test 覆蓋的 integration boundary。121 之後的 integration testing 就是補這個層級。

12. Guided exercise:Success / Empty / Error Fake

const successRepo = { ... };
const emptyRepo = { ... };
const errorRepo = { ... };

同一個 loadStudents(repo) 跑三個 dependencies,驗證三種 contract。

13. Independent exercise:Mock Save Contract

寫 submitStudent(input, repo):valid 才呼叫 repo.save;invalid 要 throw 且 save.calls.length 維持 0。

const save =
  mockFn();

const repo = { save };

14. Challenge:Latest-Wins Async Test

為 132 的 latest-wins helper 建兩個 controllable promises,故意讓第二個先完成、第一個晚完成,證明舊結果回 "stale" 而不覆蓋最新值。

first starts
second starts
second resolves
first resolves later

Project checkpoint:Final Project Async Test Matrix

load/save
├─ success
├─ empty
├─ rejection
├─ cancellation
└─ stale completion

same app logic
controlled boundaries

Final Project 不需要 mock DOM 每個細節;pure rules 用 unit tests,async boundaries 用 fakes/mocks,整體 UI 再用 workshop acceptance checks。

15. Vocabulary / Summary

16. Further reading

Knowledge check · Mastery

  1. 解釋 fake、mock 與 production dependency 的差異。
  2. 完成 success/error/cancelled async tests。
  3. 判斷哪些 interaction 值得 assert、哪些是 implementation detail。
  4. 完成頁面下方 Real Test Runner async/mock missions。