บทที่ 13 · Part 3 — UI and API Capabilities
Network Control and Dependencies
รอ response อย่างมีเหตุผล mock เฉพาะ boundary และใช้ route/HAR จำลอง failure ที่สร้างจริงได้ยาก
Network Control and Dependencies
notification service ตอบ 503 เพียงบางครั้งใน test environment ทีมจึงไม่เคยเห็น UI state “จองสำเร็จแต่ส่ง reminder ไม่สำเร็จ” แบบ deterministic การควบคุม network ช่วยสร้าง rare state ได้ แต่ mock มากเกินไปจะทำให้ test ผ่านแม้ contract จริงเปลี่ยน
จบบทนี้คุณจะ
ใช้ waitForResponse, route, fulfill, fetch, abort และ HAR replay เลือก boundary ที่ควร mock
ลง route ก่อน request และรักษา integration coverage เพื่อจับ contract drift
รอ Response เมื่อ Response เป็น Claim
ถ้าต้องตรวจ request/response ที่ action ส่ง ให้ตั้ง promise ก่อนคลิก:
const responsePromise = page.waitForResponse((response) =>
response.url().endsWith('/api/reservations') &&
response.request().method() === 'POST',
)
await page.getByRole('button', { name: 'Reserve seat' }).click()
const response = await responsePromise
expect(response.status()).toBe(201)
await expect(page.getByRole('heading', { name: 'Reservation confirmed' })).toBeVisible()
ถ้าตรวจ visible result อย่างเดียวก็ไม่ต้องรอ network การผูก endpoint เพิ่ม coupling โดยไม่เพิ่ม evidence ใช้ network wait เมื่อ status/payload/request shape คือ behavior ที่กำลังทดสอบหรือจำเป็นต่อ diagnosis
Mock Dependency Failure ด้วย Route
ลง route ก่อน navigation/action ที่ส่ง request:
await page.route('**/api/reminders', async (route) => {
await route.fulfill({
status: 503,
contentType: 'application/json',
body: JSON.stringify({
error: { code: 'REMINDER_UNAVAILABLE' },
}),
})
})
await page.goto(`/workshops/${workshop.slug}`)
await page.getByRole('button', { name: 'Reserve seat' }).click()
await expect(page.getByRole('heading', { name: 'Reservation confirmed' })).toBeVisible()
await expect(page.getByRole('status')).toContainText(
'Reserved. Reminder will be retried later.',
)
Test นี้พิสูจน์ frontend behavior ต่อ known 503 contract ไม่ได้พิสูจน์ reminder service จริง จึงต้องมี API/integration suite ที่เรียก dependency test environment หรือ consumer contract แยก
Inspect Request ก่อน Fulfill
await page.route('**/api/reservations', async (route) => {
const payload = route.request().postDataJSON() as { workshopId: string }
expect(payload.workshopId).toBe(workshop.id)
await route.fulfill({
status: 201,
json: {
data: { id: 'res-test-001', status: 'CONFIRMED' },
},
})
})
ใช้ fixed mock id ได้เพราะอยู่ใน isolated browser test และไม่แตะ server state แต่ถ้า parallel pages แชร์ context route
ต้องแน่ใจว่า match ไม่กว้างจนจับ request ของ test อื่น Built-in test page มี context แยกต่อ test อยู่แล้ว
Modify Real Response ด้วย route.fetch()
await page.route('**/api/workshops/*', async (route) => {
const response = await route.fetch()
const body = await response.json()
body.data.seatsRemaining = 0
await route.fulfill({ response, json: body })
})
Pattern นี้รักษา headers/status ส่วนใหญ่และแก้ field ที่ต้องสร้าง state แต่ยังผูกกับ upstream availability ถ้า response schema เปลี่ยน test อาจ fail ซึ่งบางครั้งเป็นประโยชน์ ระบุชัดว่าเป็น modified integration หรือ full stub
Continue และ Abort
await page.route('**/analytics/**', (route) => route.abort())
await page.route('**/api/**', (route) => route.continue())
Abort analytics ช่วยพิสูจน์ app ยังทำ core journey ได้เมื่อ non-critical script/network หาย แต่ห้าม block dependency
จนซ่อน behavior ที่ต้องรองรับจริง Route pattern กว้างต้อง review เพราะลำดับ handlers และ popup requests อาจต่าง
ถ้าต้องครอบทุก page/popup ใช้ browserContext.route()
Service Worker Caveat
Request ที่ Service Worker intercept อาจไม่ปรากฏให้ native routing เห็น หาก suite ต้องควบคุม network ด้วย route
ให้ตั้ง serviceWorkers: 'block' ใน project นั้นตาม network documentation
แต่ test PWA/offline behavior ต้องเปิด Service Worker และใช้ strategy อื่น แยก projects เพราะ 2 claims ขัดกัน
HAR Replay
await page.routeFromHAR('tests/har/workshop-catalog.har', {
url: '**/api/catalog/**',
notFound: 'abort',
})
await page.goto('/workshops')
HAR เหมาะกับ read-heavy flow ที่มีหลาย responses และต้อง replay deterministic Matching ใช้ URL + method
และ POST payload เมื่อเกี่ยวข้อง notFound: 'abort' ทำให้ unrecorded call fail ชัด ส่วน fallback ส่งต่อ network
เลือกตาม test intent
Record/update HAR เป็น controlled maintenance task ไม่เปิด update บน CI โดยอัตโนมัติ เพราะ upstream drift อาจถูก approve เป็น baseline ใหม่โดยไม่ review HAR มี headers, cookies และ bodies ต้อง inspect/redact ก่อน commit หากมีข้อมูลอ่อนไหวให้สร้าง synthetic HAR หรือไม่ commit เลย
Mocking Decision Table
| Need | Tool | Evidence ที่ได้ | Evidence ที่ไม่ได้ |
|---|---|---|---|
| รอ create response | waitForResponse | request จริงเกิดและ status ตาม environment | deterministic rare failure |
| UI state จาก known error | route.fulfill | frontend mapping/rendering | upstream contract ยังเหมือนจริง |
| แก้ field จาก real response | route.fetch + fulfill | integration ส่วนใหญ่ + controlled variation | full real behavior ของ field ที่แก้ |
| หลาย read endpoints คงที่ | HAR replay | deterministic recorded contract | current upstream drift |
| dependency unreachable | route.abort | app network-failure behavior | server-specific error body |
Contract Drift Strategy
แยก suites อย่างน้อย:
- deterministic UI tests ที่ mock rare/error states
- API/contract tests ที่ pin schemas/error codes
- integration smoke ที่ไม่ mock critical dependencies
เมื่อ mock schema เปลี่ยน ให้ owner ของ consuming UI และ producing API review พร้อมกัน อย่าปล่อย mocks เป็น JSON ที่ไม่มี type/schema validation จนกลายเป็น fiction
รายการตรวจสอบ
- Listener/route ถูกลงก่อน request
- Mock อยู่ที่ boundary ที่ test ไม่ได้กำลังพิสูจน์
- Route scope แคบและ request assertions ตรง intent
- Service Worker policy ตรง claim ของ project
- HAR เป็น synthetic/redacted และ update ผ่าน review
- มี unmocked contract/integration coverage ของ dependency สำคัญ
สรุปบทนี้
Network control ทำ rare states ให้ reproducible แต่ทุก mock ตัด evidence บางส่วน Decision table และ unmocked suite ช่วยให้ deterministic feedback ไม่แลกกับการมองไม่เห็น contract drift