Test Independence และ Isolation
จบบทนี้แล้วคุณจะ...
- เข้าใจว่าทำไม test ต้องไม่พึ่งพากัน (independent)
- ระบุปัญหาที่เกิดจาก shared state และ test order dependency
- เข้าใจหลักการ setup/teardown และ test data isolation
- วิเคราะห์ test suite ที่มี dependency แล้วแก้ไขให้ independent
ทีมหนึ่งมี 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 — และจะเป็นปัญหาเสมอ
กฎเหล็ก: Test แต่ละตัวต้องรันเดี่ยวได้
ทุก test ต้องสามารถ:
- รันเดี่ยวได้ โดยไม่ต้องรัน test อื่นก่อน
- รันในลำดับไหนก็ได้ ผลลัพธ์เหมือนกัน
- รัน parallel ได้ โดยไม่กระทบ test อื่น
ถ้าทำไม่ได้แม้แต่ข้อเดียว = test นั้นมี dependency
ปัญหาที่เกิดจาก dependency
| ปัญหา | ผลกระทบ |
|---|---|
| Flaky tests | test ผ่านบ้างไม่ผ่านบ้าง ขึ้นกับลำดับที่รัน |
| ไม่สามารถรัน parallel | test suite ช้า เพราะต้องรันทีละตัว |
| Debug ยาก | test fail แต่ไม่รู้ว่าเพราะ code พัง หรือเพราะ test อื่นทำข้อมูลเปลี่ยน |
| Cascade failure | test ตัวหนึ่งพัง ทำให้ test อื่นอีก 10 ตัวพังตาม |
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 ต้อง:
- สร้าง data ของตัวเอง ใน Arrange (ไม่พึ่ง test อื่น)
- ทำความสะอาดหลังเสร็จ ใน Teardown (ไม่ทิ้งขยะไว้ให้ test อื่น)
- ใช้ data ที่ unique ไม่ซ้ำกับ test อื่น (เช่น ใช้ random ID)
โครงสร้างที่ช่วยให้ 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 จบ |
ทำไม 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 ต่อไปนี้ ระบุปัญหา แล้วแก้ไขให้ 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
คำถาม
- ระบุ dependency ระหว่าง tests
- ถ้ารัน Test 3 ก่อน Test 2 จะเกิดอะไรขึ้น?
- ถ้ารัน Test 1 fail จะกระทบ test อื่นอย่างไร?
- แก้ไข 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 ที่ต้องแก้