CupCode.cc
Section 03 · Module 04 · Lesson 02 · ~15-20 นาที

Test Independence และ Isolation

เวลาเรียน: 15-20 นาที ต้องรู้มาก่อน: Lesson 07 - Arrange-Act-Assert รูปแบบ: อ่าน + แบบฝึกสั้น ๆ

จบบทนี้แล้วคุณจะ...

  • เข้าใจว่าทำไม test ต้องไม่พึ่งพากัน (independent)
  • ระบุปัญหาที่เกิดจาก shared state และ test order dependency
  • เข้าใจหลักการ setup/teardown และ test data isolation
  • วิเคราะห์ test suite ที่มี dependency แล้วแก้ไขให้ independent
1 · เรื่องจริง: ผ่านเมื่อรันตามลำดับ พังเมื่อรันสลับ

ทีมหนึ่งมี automated test suite 50 ตัว ทุกตัวผ่านสีเขียวเสมอ จนกระทั่งวันหนึ่งมีคนเปลี่ยน config ให้รันแบบ parallel (หลายตัวพร้อมกัน) แทนที่จะรัน sequential (ทีละตัว)

ผลลัพธ์: 15 ตัวจาก 50 ตัว fail ทันที

สาเหตุ:

  • Test #5 สร้าง user "testuser1" ไว้ใน database
  • Test #12 ใช้ "testuser1" โดยไม่ได้สร้างเอง — แต่พึ่ง Test #5 สร้างให้
  • Test #20 ลบ "testuser1" ออก — ทำให้ test อื่นที่ต้องใช้ user นี้พัง
  • Test #30 ตรวจว่า database มี user 5 คน — แต่จำนวนขึ้นอยู่กับว่า test อื่นรันไปแล้วกี่ตัว

เมื่อรันตามลำดับ ทุกอย่างบังเอิญทำงานได้ แต่พอรันสลับลำดับหรือรัน parallel ก็พังทันที

บทเรียน: ถ้า test ต้องรันตามลำดับถึงจะผ่าน แปลว่า test เหล่านั้นไม่ independent — และจะเป็นปัญหาเสมอ

2 · ทำไม Test ต้อง Independent

กฎเหล็ก: Test แต่ละตัวต้องรันเดี่ยวได้

ทุก test ต้องสามารถ:

  • รันเดี่ยวได้ โดยไม่ต้องรัน test อื่นก่อน
  • รันในลำดับไหนก็ได้ ผลลัพธ์เหมือนกัน
  • รัน parallel ได้ โดยไม่กระทบ test อื่น

ถ้าทำไม่ได้แม้แต่ข้อเดียว = test นั้นมี dependency

ปัญหาที่เกิดจาก dependency

ปัญหา ผลกระทบ
Flaky tests test ผ่านบ้างไม่ผ่านบ้าง ขึ้นกับลำดับที่รัน
ไม่สามารถรัน parallel test suite ช้า เพราะต้องรันทีละตัว
Debug ยาก test fail แต่ไม่รู้ว่าเพราะ code พัง หรือเพราะ test อื่นทำข้อมูลเปลี่ยน
Cascade failure test ตัวหนึ่งพัง ทำให้ test อื่นอีก 10 ตัวพังตาม
3 · Shared State: ศัตรูหมายเลข 1

State ที่ test แชร์กัน

  • Database records — test ตัวหนึ่งสร้าง data ที่ test อื่นใช้
  • Global variables — ค่าที่ถูกเปลี่ยนในระหว่าง test
  • Files — test สร้าง/ลบ file ที่ test อื่นต้องการ
  • Browser cookies/session — login state ที่ค้างจาก test ก่อนหน้า
  • Environment variables — ค่า config ที่ถูกเปลี่ยนใน test

วิธีแก้: Each test owns its data

ทุก test ต้อง:

  1. สร้าง data ของตัวเอง ใน Arrange (ไม่พึ่ง test อื่น)
  2. ทำความสะอาดหลังเสร็จ ใน Teardown (ไม่ทิ้งขยะไว้ให้ test อื่น)
  3. ใช้ data ที่ unique ไม่ซ้ำกับ test อื่น (เช่น ใช้ random ID)
4 · Setup / Teardown

โครงสร้างที่ช่วยให้ test สะอาด

Setup (Before each test):

  • สร้าง test data ที่ test นี้ต้องการ
  • Reset state ให้อยู่ในสภาพที่รู้

Teardown (After each test):

  • ลบ data ที่สร้างไว้
  • Reset state กลับเหมือนเดิม
BEFORE EACH TEST:
  clear_database()
  create_test_data()

TEST "..."
  // Arrange (เพิ่มจาก setup)
  // Act
  // Assert

AFTER EACH TEST:
  clear_database()
  close_browser()

เทคนิคที่ช่วยได้

เทคนิค คำอธิบาย
Database transaction rollback เริ่ม transaction ก่อน test, rollback หลัง test — data ไม่ถูก commit จริง
Factory pattern สร้าง test data ด้วย factory ที่ generate unique data ทุกครั้ง
Docker containers สร้าง database ใหม่ทั้ง container สำหรับแต่ละ test run
In-memory database ใช้ database ใน memory ที่หายไปเมื่อ test จบ
5 · Parallel Execution Readiness

ทำไม parallel ถึงสำคัญ

Test suite 500 ตัว ถ้ารัน sequential ใช้เวลา 30 นาที ถ้ารัน parallel 5 threads ใช้เวลาแค่ 6 นาที — เร็วขึ้น 5 เท่า

แต่ parallel ได้ก็ต่อเมื่อ test ทุกตัว independent — ถ้ามี shared state จะพังทันที

Checklist ก่อนรัน parallel

  • ทุก test สร้าง data ของตัวเอง
  • ไม่มี test ที่เปลี่ยน global state
  • ไม่มี test ที่ depend on execution order
  • Database records ใช้ unique identifiers
  • File operations ใช้ temporary directories ที่แยกกัน
แบบฝึก: วิเคราะห์ Test Suite ที่มี Dependency

วิเคราะห์ test suite ต่อไปนี้ ระบุปัญหา แล้วแก้ไขให้ independent

TEST SUITE: User Management

Test 1: "สร้าง user ใหม่"
  create_user("john", "[email protected]")
  EXPECT database has user "john"

Test 2: "แก้ไข email ของ user"
  update_email("john", "[email protected]")
  EXPECT user "john" has email "[email protected]"

Test 3: "ลบ user"
  delete_user("john")
  EXPECT database does NOT have user "john"

Test 4: "ค้นหา user ที่ไม่มีในระบบ"
  result = search_user("john")
  EXPECT result is empty

คำถาม

  1. ระบุ dependency ระหว่าง tests
  2. ถ้ารัน Test 3 ก่อน Test 2 จะเกิดอะไรขึ้น?
  3. ถ้ารัน Test 1 fail จะกระทบ test อื่นอย่างไร?
  4. แก้ไข test suite ให้ independent
ดูคำตอบแนะนำ

1. Dependencies:

  • Test 2 ต้องพึ่ง Test 1 (ต้องมี user "john" ก่อน)
  • Test 3 ต้องพึ่ง Test 1 (ต้องมี user "john" ก่อน) และต้องรันหลัง Test 2
  • Test 4 ต้องพึ่ง Test 3 (ต้อง user "john" ถูกลบแล้ว)
  • ทุก test แชร์ state เดียวกัน (user "john")

2. Test 3 ก่อน Test 2:

  • Test 3 ลบ "john" → Test 2 จะ fail เพราะไม่เจอ user "john"

3. Test 1 fail:

  • Test 2, 3, 4 จะ fail ทั้งหมด → cascade failure

4. แก้ไขให้ independent:

Test 1: "สร้าง user ใหม่"
  // Arrange: ไม่ต้องเตรียมอะไร
  // Act
  create_user("john_test1", "[email protected]")
  // Assert
  EXPECT database has user "john_test1"
  // Teardown
  delete_user("john_test1")

Test 2: "แก้ไข email ของ user"
  // Arrange: สร้าง user ของตัวเอง
  create_user("john_test2", "[email protected]")
  // Act
  update_email("john_test2", "[email protected]")
  // Assert
  EXPECT user "john_test2" has email "[email protected]"
  // Teardown
  delete_user("john_test2")

Test 3: "ลบ user"
  // Arrange: สร้าง user ของตัวเอง
  create_user("john_test3", "[email protected]")
  // Act
  delete_user("john_test3")
  // Assert
  EXPECT database does NOT have user "john_test3"

Test 4: "ค้นหา user ที่ไม่มีในระบบ"
  // Arrange: ไม่ต้องสร้างอะไร + ตรวจว่า user นี้ไม่มีจริง
  // Act
  result = search_user("nonexistent_user_xyz")
  // Assert
  EXPECT result is empty

แต่ละ test สร้าง data ของตัวเอง ใช้ชื่อไม่ซ้ำกัน ทำความสะอาดหลังเสร็จ รันลำดับไหนก็ผ่าน!

สิ่งที่ต้องเอาติดตัวไป
  • Test ทุกตัวต้อง independent — รันเดี่ยวได้ รันสลับได้ รัน parallel ได้
  • Shared state (database records, global variables, cookies) เป็นศัตรูหมายเลข 1 ของ test independence
  • ทุก test ต้อง สร้าง data ของตัวเอง ใน Arrange และ ทำความสะอาด ใน Teardown
  • Setup/Teardown ช่วยให้ test สะอาด — ใช้ transaction rollback, factory pattern, หรือ in-memory database
  • ถ้า test ต้องรันตามลำดับถึงจะผ่าน = สัญญาณเตือนว่ามี dependency ที่ต้องแก้