ตอนที่ 3 ของซีรีส์ Testing ใน Laravel ต่อจากของใหม่ใน Pest 5
พอโปรเจกต์โตขึ้น test ก็ช้าลงตาม โปรเจกต์ลูกค้าที่ใช้ Pest 5 ตัวหนึ่งมี test ราวสองพันตัว รันทั้งชุดใช้ 201 วินาที ระหว่างแก้โค้ดเราแก้ class เดียวแล้วต้องรอสามนาทีกว่าจะรู้ว่าพังอะไรไหม สุดท้ายก็เลือกรันแค่ไฟล์ test ที่คิดว่าเกี่ยว แล้วไปเจอทีหลังใน CI ว่าพลาดไฟล์อื่น
TIA ให้ Pest เป็นคนเลือกแทนเรา
Pest จดว่า test แต่ละตัววิ่งผ่านไฟล์ไหน
รอบแรกที่ใส่ --tia Pest รันทั้งชุดโดยเปิด code coverage ไว้ coverage บอกได้ว่าระหว่าง test แต่ละตัวทำงาน PHP เข้าไปอ่านไฟล์ไหนบ้าง Pest เก็บความสัมพันธ์นี้ไว้เป็น graph พร้อม hash ของแต่ละไฟล์และผลของ test ทุกตัว
รอบต่อไป Pest ดูว่าไฟล์ไหนเปลี่ยนจาก git แล้วรันเฉพาะ test ที่วิ่งผ่านไฟล์นั้น ตัวที่เหลือไม่ได้รันจริง Pest เอาผลที่จำไว้มาแสดงแทน บรรทัดสรุปจึงมีคำใหม่สามคำ
Tests: 774 passed (2658 assertions, 7 affected, 2 uncached, 765 replayed)
Duration: 3.92s
- affected รันใหม่เพราะไฟล์ที่ test ตัวนั้นใช้เปลี่ยน
- uncached รันเพราะยังไม่มีผลเดิมเก็บไว้ เช่น test ที่เพิ่งเขียน
- replayed ไม่ได้รัน ใช้ผลจากรอบก่อน
Pest ตัดช่องว่าง comment และ docblock ออกก่อนคิด hash แก้ comment หรือรัน Pint ทั้งโปรเจกต์จึงไม่ทำให้ test ตัวไหนต้องรันใหม่ ส่วนไฟล์ที่ไม่ใช่ PHP อย่าง migration, Blade และ component ของ Inertia ก็มีกฎของตัวเอง เช่น migration ที่เปลี่ยนจะรัน test ที่เคย query ตารางนั้น
ตัวเลขจากโปรเจกต์ลูกค้าที่ใช้ Pest 5
| รอบ | เวลา |
|---|---|
| รันเต็มแบบปกติ ไม่มี coverage | 201 วินาที |
| รอบแรกของ TIA รันเต็มภายใต้ pcov | 309 วินาที |
| ไม่ได้แก้อะไร | 3.4 วินาที |
| แก้แค่ comment หรือจัด format | 3.6 วินาที ไม่มี test ไหนรันจริง |
| แก้ class เดียวในโค้ดแอป | 10.1 วินาที รัน 27 test จาก 6 ไฟล์ |
แก้ไฟล์ที่ทุก test โหลด เช่น routes/api.php |
รันเต็มทั้งชุด |
รอบแรกช้ากว่าปกติราวครึ่งหนึ่ง เพราะ coverage ต้องคอยจดทุกบรรทัดที่ทำงาน แต่จ่ายครั้งเดียว หลังจากนั้นวงแก้โค้ดแล้วรัน test เหลือไม่กี่วินาที
แถวสุดท้ายก็ถูกต้องแล้ว ไฟล์ที่ทุก test ผ่าน TIA ช่วยอะไรไม่ได้ ไฟล์ config ก็เหมือนกัน แก้ config/app.php แล้ว Pest จะรันทั้งชุด เพราะพิสูจน์ไม่ได้ว่า test ไหนไม่เกี่ยว
ติดตั้ง pcov ไว้เป็น coverage driver
TIA ต้องมี coverage driver ตัวใดตัวหนึ่งระหว่าง pcov กับ Xdebug เราเลือก pcov เพราะทำแค่เก็บ coverage จึงเร็วกว่า Xdebug มาก
# Ubuntu หรือ Debian
sudo apt install php8.4-pcov
# macOS ที่ลง PHP ด้วย Homebrew
pecl install pcov
ถ้าใช้ Laragon หรือ XAMPP บน Windows แล้วเปิด Xdebug ไว้อยู่แล้ว ใช้ Xdebug ไปก่อนก็ได้ รอบแรกจะช้ากว่า
pcov มีเรื่องที่ต้องระวังสองเรื่อง
- pcov ใช้กับ JIT ไม่ได้ PHP จะปิด JIT เองแล้วเตือนว่า
JIT is incompatible with third party extensions…บน server จึงเปิด pcov เฉพาะ PHP CLI ไม่ใส่ให้ PHP-FPM ที่เสิร์ฟเว็บ ตั้งpcov.enabled=0ไว้เป็นค่าเริ่มต้น แล้วเปิดตอนรัน test ด้วยphp -d pcov.enabled=1ใส่-d opcache.jit=disableไปด้วยก็จะไม่เห็นคำเตือนนี้อีก - ถ้าโหลด pcov แต่
pcov.enabled=0Pest ยังเลือก pcov เป็น driver แต่เก็บ coverage ไม่ได้สักบรรทัด graph ที่ได้จึงว่างเปล่าโดยไม่มี error
ตั้ง composer script ไว้รันในเครื่อง
โปรเจกต์ลูกค้าทั้งสองตัวตั้ง script นี้ไว้
"scripts": {
"test:tia": [
"@php artisan config:clear --ansi",
"@php vendor/bin/pest --tia --locally --compact"
]
}
config:clear กันไม่ให้ config ที่ cache ไว้จากตอนพัฒนาไปปนกับค่าของ test ส่วน --locally บอก Pest ว่าเปิด TIA เฉพาะในเครื่องนักพัฒนา ถ้ารันใน CI ให้ข้ามไปรันเต็มชุดตามปกติ Pest รู้ว่ากำลังรันใน CI จาก environment variable อย่าง CI หรือ GITHUB_ACTIONS ที่ระบบ CI ตั้งไว้ให้
flag อื่นที่ใช้บ่อย
--freshทิ้ง graph เดิมแล้วจดใหม่ ใช้เมื่อสงสัยว่า graph ไม่ตรงกับโค้ดแล้ว--no-tiaปิด TIA รอบเดียว--baselineบอก path ที่เก็บ graph แล้วจบ ปกติอยู่ที่~/.pest/tia/นอก repo จึงไม่ต้องเพิ่มอะไรใน.gitignore
บอก TIA ถึงไฟล์ที่มองไม่เห็นเอง
TIA รู้จักไฟล์จากสิ่งที่ coverage เห็น มีไฟล์สองแบบที่ coverage ไม่เห็น ในโปรเจกต์ลูกค้าเราจึงเพิ่มเองใน tests/Pest.php
pest()->tia()->watch([
'database/snapshots/*.sql.gz' => 'tests',
'tests/Pest.php' => 'tests',
]);
- ไฟล์ snapshot ของฐานข้อมูล โปรเจกต์นี้โหลดตารางและข้อมูลตั้งต้นจากไฟล์ SQL ที่ commit ไว้ ไม่ได้รัน migration ทุกรอบ TIA รู้จักแค่
database/migrationsถ้าไม่บอก พอเราแก้ snapshot แล้ว TIA ก็จะเอาผลเดิมมาแสดงทั้งที่ฐานข้อมูลเปลี่ยนไปแล้ว วิธีจัดฐานข้อมูลแบบนี้อยู่ในตอนที่ 4 tests/Pest.phpเอง Pest อ่านไฟล์นี้ก่อนเริ่มเก็บ coverage จึงไม่มี test ไหนผูกกับไฟล์นี้เลย ทั้งที่ในไฟล์มี base class,beforeEachและ helper ที่ทุก test ใช้
=> 'tests' แปลว่าไฟล์ที่ตรงกับ pattern เปลี่ยนเมื่อไร ให้รัน test ทั้งโฟลเดอร์ tests วิธีเช็กว่า pattern ใช้ได้จริงคือแก้ไฟล์นั้นแล้วดูบรรทัดสรุป ต้องเห็น affected ไม่ใช่ replayed
ผลเขียวจาก TIA ไม่ได้แปลว่า test ผ่านทั้งชุด
test ที่ขึ้นว่า replayed ไม่ได้รันในรอบนั้น ผลที่เห็นคือผลจากตอนที่ Pest จดไว้ ถ้า graph พลาดอะไรไป TIA ก็ไม่มีทางรู้ เราจึงใช้ TIA ระหว่างแก้โค้ดเท่านั้น ก่อน push รันทั้งชุดด้วย php artisan test อีกรอบ และ CI รันทั้งชุดทุกครั้งโดยไม่ใส่ --tia ตามที่เอกสารของ Pest แนะนำ
อีกสองกรณีที่ทำให้หลงคิดว่า TIA ทำงาน
- ใส่
--tiaแต่เครื่องไม่มี pcov หรือ Xdebug Pest ยังขึ้นข้อความว่าเปิดโหมด TIA แต่จดอะไรไม่ได้และรันทั้งชุดทุกครั้ง สังเกตจากบรรทัดสรุป ถ้าไม่มีคำว่า affected หรือ replayed แปลว่า TIA ไม่ได้ทำงาน - ใส่
--locallyใน workflow ของ CI ในโปรเจกต์ลูกค้าเราเคยลองเปิด TIA บน server ที่รัน test ก่อน deploy แล้วใส่--locallyไว้ด้วย Pest ข้าม TIA ตามที่สั่ง แต่ pcov ยังเปิดอยู่ รอบนั้นจึงช้าที่สุด 688 วินาที เทียบกับราว 201 วินาทีตอนไม่มี pcov
ถ้าอยากให้ทั้งทีมไม่ต้องรอรอบแรกเอง Pest มี workflow ให้ CI จด graph ตอน merge เข้า main แล้วเครื่องนักพัฒนาดึงไปใช้ผ่าน gh ได้ รองรับ GitHub และ GitLab ตั้งแต่ Pest 5.2 แต่ต้องตั้ง workflow แยกอีกตัว ทีมเล็กใช้ --locally แล้วให้แต่ละเครื่องจดเองก็พอ
ให้ AI agent ใช้ TIA ระหว่างแก้โค้ด
TIA เหมาะกับงานที่ AI agent ทำมาก เพราะ agent แก้โค้ดแล้วรัน test วนหลายสิบรอบต่องาน ถ้ารอบละสามนาที งานชิ้นเดียวก็หมดไปเป็นชั่วโมง เขียนลำดับไว้ใน CLAUDE.md หรือ AGENTS.md ของโปรเจกต์ให้ชัด
## Tests
- ระหว่างแก้โค้ด รัน `composer test:tia` หลังแก้แต่ละครั้ง
- ก่อนบอกว่างานเสร็จ รัน `php artisan test --compact` ทั้งชุด แล้วรายงานผลจากรอบนั้น
- ผลเขียวจาก `test:tia` ไม่นับเป็นผลว่า test ผ่าน
บรรทัดสุดท้ายสำคัญที่สุด ถ้าไม่เขียนไว้ agent อาจเห็น TIA ขึ้นเขียวแล้วรายงานว่า test ผ่านหมด ทั้งที่รอบนั้น test เกือบทั้งหมดไม่ได้รันจริง
ตอนต่อไปเป็นเรื่องฐานข้อมูลสำหรับ test ที่เราใช้ snapshot กับ DatabaseTransactions แทน RefreshDatabase ถ้าเพิ่งเข้ามาที่ตอนนี้ รายชื่อทุกตอนอยู่ท้ายตอนแรกครับ






