บทที่ 16 · Part 4 — Reliability and Operations
Debugging and Reporting
ใช้ UI Mode, Inspector, trace, HTML report, steps และ redacted attachments เพื่อหาสาเหตุจาก artifact เดียว
Debugging and Reporting
Failure ที่บอกเพียง expected 201, received 422 บังคับคนแก้ rerun และหวังว่าอาการจะเกิดซ้ำ Test artifact ที่ดี
ตอบได้ว่า test ทำอะไร, browser เห็นอะไร, request/response เป็นอย่างไร และ failure อยู่ phase ไหน โดยไม่เก็บ secret
มากกว่าที่จำเป็น
จบบทนี้คุณจะ
ใช้ UI Mode, Inspector, codegen, trace และ reporters เลือก artifact ตาม failure mode สร้าง redacted API attachment เฉพาะเมื่อ fail และวาง workflow triage ที่เริ่มจาก evidence
Local Authoring Tools
npx playwright test --ui
npx playwright test tests/ui/reservations/reserve.spec.ts --debug
npx playwright codegen http://127.0.0.1:4173/workshops
UI Mode ให้ filter ตาม project/tag/status, watch rerun, timeline, DOM snapshots, network, console และ attachments
เหมาะกับ authoring/reproduce Inspector จาก --debug ช่วย step/pick locator กับ live page ส่วน codegen record
actions และเสนอ role/text/test-id locator เป็น draft
Codegen ไม่รู้ risk, test data lifecycle, oracle หรือ abstraction ของ project ต้อง refactor title, setup, steps, locators และ assertions ก่อน commit ไม่เก็บ generated mega-flow เป็น final test
Trace บน CI
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
}
Trace บันทึก timeline, actions, DOM snapshots, network, console และ assertions เปิด local artifact ด้วย:
npx playwright show-trace path/to/trace.zip
on-first-retry ทำให้ first execution เร็วและเก็บ trace ตอน retry ที่ต้องวิเคราะห์ หาก failure เกิดเฉพาะ attempt แรก
อาจต้องเปิด retain-on-failure ชั่วคราวใน diagnosis branch แต่ trace ทุก test เพิ่ม storage/เวลาและข้อมูลอ่อนไหว
Steps และ Boxed Location
test.step() ทำ report เป็น narrative และรองรับ box เพื่อให้ stack ชี้ call site ของ high-level action:
await test.step(
'Reserve the selected workshop',
async () => {
await catalog.actions.reserve(workshop.title)
await reservation.assertions.verifyConfirmed(workshop.title)
},
{ box: true },
)
ใช้ box เมื่อ helper ภายในมีหลาย locators แต่ caller action สำคัญ อย่าห่อทุกบรรทัดเพราะ report จะกลายเป็น noise
Redacted API Attachment Fixture
ตัวอย่างนี้ให้ wrapper method เดียว fetch เพื่อให้ type/behavior ชัด และอ่าน APIResponse.body() ตาม API ของ
Playwright โดยไม่สมมติว่าเป็น browser Fetch Response:
import {
test as base,
type APIRequestContext,
type APIResponse,
} from '@playwright/test'
type FetchOptions = Parameters<APIRequestContext['fetch']>[1]
type Exchange = { method: string; url: string; status: number; request: unknown; response: unknown }
type RecordedApi = { fetch: (url: string, options?: FetchOptions) => Promise<APIResponse> }
const sensitive = new Set(['authorization', 'cookie', 'password', 'token'])
const redact = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(redact)
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, item]) =>
sensitive.has(key.toLowerCase()) ? [key, '[REDACTED]'] : [key, redact(item)],
),
)
}
return value
}
export const test = base.extend<{ api: RecordedApi }>({
api: async ({ playwright, baseURL }, use, testInfo) => {
const context = await playwright.request.newContext({ baseURL })
const exchanges: Exchange[] = []
await use({
fetch: async (url, options) => {
const response = await context.fetch(url, options)
const raw = (await response.body()).toString('utf8')
let responseBody: unknown = raw
try { responseBody = JSON.parse(raw) } catch { /* keep text */ }
exchanges.push({
method: options?.method ?? 'GET',
url: response.url(),
status: response.status(),
request: redact(options?.data ?? null),
response: redact(responseBody),
})
return response
},
})
if (testInfo.status !== testInfo.expectedStatus && exchanges.length > 0) {
await testInfo.attach('http-exchanges.json', {
body: JSON.stringify(exchanges, null, 2),
contentType: 'application/json',
})
}
await context.dispose()
},
})
Project จริงต้องขยาย sensitive keys ตาม data classification และ redact nested headers/query/body ก่อนเก็บ ตัวอย่างไม่บันทึก request headers เพื่อลด risk Passing runs ไม่แนบไฟล์เพื่อลด noise/storage
Reporter ตามผู้บริโภค
| Reporter | เหมาะกับ |
|---|---|
| list/line | console feedback ระหว่าง run |
| HTML | human exploration, steps, traces, screenshot diffs |
| JSON/JUnit | machine/CI/test management integration |
| blob | intermediate result สำหรับ merge หลาย shards |
Reporter หลายตัวใช้พร้อมกันได้ HTML เปิด never บน CI Blob ไม่ใช่ report ปลายทางสำหรับคน แต่เก็บ attachments
เพื่อ merge ภายหลัง
Triage Workflow
- อ่าน test title, project, retry classification และ failing step
- เปิด trace ดู actionability, DOM snapshot, console และ network ณ failure
- ตรวจ screenshot/visual diff ถ้า claim เกี่ยว layout
- ตรวจ redacted HTTP exchange และ correlate request id กับ server logs
- rerun test เดี่ยว
--repeat-eachแล้วเปรียบเทียบ workers/projects - จัดประเภท product defect, test defect, environment, data collision หรือ known dependency
- แก้ root cause และเพิ่ม assertion/artifact เฉพาะที่ช่วยป้องกัน recurrence
ห้าม rerun pipeline จนเขียวโดยไม่บันทึก first failure เพราะทำลาย evidence ที่ดีที่สุด
Artifact policy ต้องมีเจ้าของ
Trace/screenshot/video/HAR/attachment อาจบันทึก DOM, session, URL และข้อมูลผู้ใช้ ระยะเก็บ, สิทธิ์เข้าถึง, การ redact และการลบต้องมาจาก security/data owner พร้อมชื่อและวันที่ ไม่ให้ผู้เขียน test เดาตัวเลขเอง
รายการตรวจสอบ
- Codegen output ถูก review เป็น behavior test ไม่ใช่ commit ตรง
- CI เก็บ trace/screenshot ตาม failure/retry policy
- Steps ชี้ phase และไม่ละเอียดจน noise
- HTTP attachment redact ก่อน serialize และไม่บันทึก headers โดยไม่จำเป็น
- Reporter ตรงกับ human/machine/merge consumer
- Triage เริ่มจาก first-failure evidence ไม่ใช่ blind rerun
สรุปบทนี้
Report ที่ดีลดเวลาจาก red build ถึง root cause Steps บอก where, trace บอก what happened และ redacted exchange บอก boundary response โดย artifact security เป็นส่วนของ test design ตั้งแต่ต้น