บทที่ 9 · Part 2 — Test Architecture

Test Data and Environments

ใช้ builders, unique data, API setup และ cleanup ownership เพื่อให้ suite parallel-safe และรันซ้ำได้

Test Data and Environments

Suite ที่ใช้ workshop ชื่อ Test Workshop และ learner คนเดียวร่วมกันจะผ่านเมื่อรันทีละไฟล์ แต่พังเมื่อ CI parallelize: test หนึ่งปิดรอบขณะที่อีก test กำลังจอง Data design จึงเป็นส่วนของ test architecture ไม่ใช่ cleanup detail หลังเขียน spec เสร็จ

จบบทนี้คุณจะ

สร้าง valid-by-default builders, unique namespace, API preconditions และ cleanup policy แยก data ที่ test เป็นเจ้าของออกจาก shared reference data พร้อมป้องกัน suite จาก environment ผิด

Tests ต้องเป็นเจ้าของ Mutable Data

แต่ละ test ควรสร้าง resource ที่แก้ไขเอง ใช้ unique id/reference และลบหรือ expire ตาม policy ไม่พึ่ง row ที่ “น่าจะมีอยู่” ใน shared staging database Reference data ที่ read-only เช่นรายชื่อระดับความยาก อาจแชร์ได้ถ้ามี version/owner แต่ reservation, user preference และ workshop state ต้อง isolate

กำหนด namespace ช่วย debug และ cleanup:

export const uniqueRef = (kind: string) =>
  `pw-${kind}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`

Timestamp ช่วยค้นหา UUID กัน collision Prefix บอกว่าเป็น automation data แต่ไม่ใช้ prefix เป็นเหตุผลลบแบบ กว้างโดยไม่มี environment guard และ age threshold

Builder ที่ Valid by Default

Builder ทำให้ทุก test เริ่มจาก payload ที่ success และ override เฉพาะ field ที่กำลังทดสอบ:

export type WorkshopInput = {
  title: string
  startsAt: string
  capacity: number
  level: 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED'
  reference: string
}

export const aWorkshop = (
  overrides: Partial<WorkshopInput> = {},
): WorkshopInput => ({
  title: `Browser Testing ${crypto.randomUUID().slice(0, 8)}`,
  startsAt: '2030-06-15T09:00:00.000Z',
  capacity: 20,
  level: 'INTERMEDIATE',
  reference: uniqueRef('workshop'),
  ...overrides,
})

Negative test เล่าเรื่องผ่าน override:

const fullWorkshop = aWorkshop({ capacity: 0 })
const invalidWorkshop = aWorkshop({ title: '' })
const boundaryWorkshop = aWorkshop({ capacity: 1 })

1 builder ต่อ resource และ compose เมื่อ nested เช่น aReservation({ workshop: aWorkshop() }) อย่า randomize field ที่ test ต้อง assert โดยไม่เก็บ value Random data ช่วย uniqueness ไม่ใช่ fuzzing; seed/reproduction strategy ต้องชัดถ้าใช้ faker สุ่ม format มากกว่านั้น

Setup ผ่าน API เร็วกว่า UI

Browser test เรื่อง cancellation ไม่ต้องสร้าง workshop และจองผ่าน UI ทุกครั้ง ใช้ API fixture เตรียม precondition แล้วเปิดหน้าที่ behavior เริ่ม:

test('CANCEL-001 - removes a future reservation from My workshops', async ({
  page,
  request,
}) => {
  const workshop = aWorkshop()
  const created = await request.post('/api/test-support/workshops', { data: workshop })
  const { id: workshopId } = await created.json()

  const reserved = await request.post('/api/test-support/reservations', {
    data: { workshopId, learner: 'current' },
  })
  expect(reserved.ok()).toBe(true)

  await page.goto('/my-workshops')
  // UI action และ visible result ที่ต้องการพิสูจน์
})

Test-support API ต้อง authenticate, จำกัดเฉพาะ test environment และมี owner อย่า deploy endpoint ที่ลบ/seed ได้โดยไม่มี environment guard ถ้าไม่มี API ให้ใช้ direct database helper เฉพาะ fixture และทดสอบ migration/schema แยก แต่ browser spec ไม่ควรรู้ SQL

Cleanup: Explicit, Idempotent, Observable

Teardown ควรลบ resource ที่ fixture สร้างด้วย exact ids และยอมรับ 404 เพื่อให้ idempotent ถ้าลบไม่ได้ให้ report/metrics เห็น ไม่ swallow จน environment สกปรก เครือข่ายล่มหลัง test success อาจทำ cleanup fail จึงควรมี scheduled janitor ตาม prefix + createdAt + dedicated environment เป็น safety net ไม่ใช่แทน teardown

อย่าใช้ destructive cleanup เช่น truncate shared database หรือ delete wildcard จาก variable ที่ไม่ validate ก่อน cleanup bulk ต้องยืนยัน environment id, base URL และ namespace แบบ exact

Environment Guardrails

const environment = process.env.TEST_ENVIRONMENT
if (!['local', 'preview', 'qa'].includes(environment ?? '')) {
  throw new Error('E2E tests require an approved non-production environment')
}

const baseURL = new URL(process.env.API_BASE_URL ?? '')
if (!baseURL.hostname.endsWith('.test.example')) {
  throw new Error('Refusing to run mutating tests outside the test domain')
}

Guard เป็น defense in depth Domain allowlist ตัวอย่างต้องเปลี่ยนเป็น hostname ที่องค์กรเป็นเจ้าของและ review redirect/proxy ด้วย Read-only smoke tests ต่อ production ต้องเป็น suite แยกที่ไม่ seed/cleanup และใช้ credential scope ต่ำตาม policy

Time Data ต้องชัด

อย่าใช้ “พรุ่งนี้จากเวลารัน” หาก business rule/Timezone ไม่ใช่สิ่งที่ test ต้องพิสูจน์ Fixed future instant ทำให้ fixture reproducible แต่วันหนึ่งอาจกลายเป็นอดีต ให้ project กำหนด baseline ที่ review เป็นระยะหรือควบคุม clock ตาม บท Time, Parallelism and Flakiness

เก็บ timestamp เป็น ISO 8601 พร้อม timezone และ assert instant/contract ไม่ pin locale copy หากไม่ได้ทดสอบ localization

Data Table ใช้เมื่อ Cases มีโครงเดียวกัน

const cases = [
  { name: 'zero capacity', capacity: 0, status: 422, code: 'CAPACITY_TOO_SMALL' },
  { name: 'one over maximum', capacity: 501, status: 422, code: 'CAPACITY_TOO_LARGE' },
]

for (const scenario of cases) {
  test(`CREATE-VAL - rejects ${scenario.name}`, async ({ request }) => {
    const response = await request.post('/api/workshops', {
      data: aWorkshop({ capacity: scenario.capacity }),
    })
    expect(response.status()).toBe(scenario.status)
    expect((await response.json()).error.code).toBe(scenario.code)
  })
}

Case title ต้อง unique และ report อ่านได้ ถ้าแต่ละ case ต้อง setup/assert ต่างกันมากให้เขียน test แยก

Artifact และ Privacy

Generated email, profile, uploaded file และ response อาจเป็นข้อมูลส่วนบุคคลแม้อยู่ test environment ใช้ synthetic data ไม่ copy production snapshot โดยไม่ anonymize ตาม policy Attach เฉพาะสิ่งจำเป็นและ redact token/cookie/password ก่อนบันทึก รายละเอียด security อยู่ในบท auth และ reporting

รายการตรวจสอบ

  • Mutable resource unique และ test/fixture เป็นเจ้าของ
  • Builder zero-argument สร้าง valid input เสมอ
  • Override แสดง field ที่เป็นเรื่องของ test
  • Setup ใช้ seam เร็วที่สุดโดยไม่เปลี่ยน claim
  • Cleanup ใช้ exact ids, idempotent และมี janitor safety net
  • Mutating suite มี non-production guard หลายชั้น
  • Test data เป็น synthetic และ artifacts ถูก minimize/redact

สรุปบทนี้

Parallel-safe suite เริ่มจาก ownership ของ data Builder ทำ intent ชัด ส่วน API setup และ exact cleanup ลดเวลาโดยไม่ลดหลักฐาน UI Environment guardrail ป้องกันความผิดพลาดที่ test code อย่างเดียวรับไม่ไหว

อ่านเพิ่มเติม