ให้ AI เขียน Unit Test แบบที่มีค่าจริง

การให้ AI เขียน unit test เป็นงานที่ดูเหมือนง่ายและคุ้มที่สุดงานหนึ่ง สั่งไปประโยคเดียวก็ได้เทสต์มาหลายสิบเคส รันแล้วเขียวทั้งหมด แต่ถ้าลองอ่านดี ๆ จะพบว่าเทสต์จำนวนมากไม่ได้ทดสอบอะไรเลย บางตัวคัดลอกสูตรจากโค้ดมาคำนวณ expected ซ้ำ บางตัวเช็กแค่ว่าไม่ error บทความนี้จะอธิบายว่าเทสต์ที่มีค่าจริงหน้าตาเป็นอย่างไร วิธีคิดแบบ mutation เพื่อวัดว่าเทสต์จับบั๊กได้ไหม วิธีสั่ง AI ให้หาเคส edge และตัวอย่าง Codeception กับ PHPUnit ที่ใช้ได้ในโปรเจกต์ Yii2
เทสต์ที่ "แค่ผ่าน" ไม่พอ
เป้าหมายของเทสต์ไม่ใช่การได้สีเขียว แต่คือการ เป็นสีแดงเมื่อโค้ดผิด เทสต์ที่ไม่มีวันแดงก็ไม่ต่างจากไม่มีเทสต์ แถมยังสร้างความมั่นใจผิด ๆ
ลักษณะเทสต์ไร้ค่าที่ AI มักสร้างเมื่อไม่ได้กำหนดอะไร
- เช็กแค่ชนิดข้อมูล เช่น
assertIsInt($result)โดยไม่สนว่าค่าเป็นเท่าไร - คัดลอกสูตรจากโค้ด มาคำนวณ expected ถ้าสูตรผิด เทสต์ก็ผิดตามและยังผ่าน
- mock ทุกอย่างจนไม่เหลืออะไรให้ทดสอบ สุดท้ายทดสอบแค่ว่า mock คืนค่าที่เราตั้งไว้
- ทดสอบแต่ happy path ไม่มีค่าขอบ ค่าว่าง ค่าติดลบ หรือ exception
- ชื่อเทสต์ไม่บอกพฤติกรรม เช่น
testFee1,testFee2ทำให้ไม่รู้ว่าพังแล้วกระทบอะไร
ตัวอย่างโค้ดที่จะทดสอบ
สมมติเรามีคลาสคำนวณค่าส่ง หน่วยเงินเป็นสตางค์ กติกาธุรกิจคือ
- ค่าส่งพื้นฐาน 40 บาท สำหรับน้ำหนักไม่เกิน 1,000 กรัม
- เกิน 1,000 กรัม คิดเพิ่ม 10 บาท ทุก 500 กรัมหรือเศษของ 500 กรัม
- พื้นที่ห่างไกลบวกเพิ่ม 30 บาท
- ยอดสินค้าตั้งแต่ 500 บาทขึ้นไป ส่งฟรี
- น้ำหนักต้องมากกว่า 0
<?php
declare(strict_types=1);
namespace common\services;
use InvalidArgumentException;
final class ShippingCalculator
{
public function fee(int $weightGrams, int $subtotalSatang, bool $remote = false): int
{
if ($weightGrams <= 0) {
throw new InvalidArgumentException('Weight must be positive');
}
if ($subtotalSatang >= 50000) {
return 0;
}
$fee = 4000;
if ($weightGrams > 1000) {
$fee += (int) ceil(($weightGrams - 1000) / 500) * 1000;
}
if ($remote) {
$fee += 3000;
}
return $fee;
}
}
ระวังเทสต์ที่คัดลอก logic จากโค้ด
ถ้าสั่ง AI ว่า "เขียนเทสต์ให้ ShippingCalculator" โดยแนบโค้ดไป มีโอกาสได้เทสต์แบบนี้
// แบบที่ไม่มีค่า: expected คำนวณด้วยสูตรเดียวกับโค้ด
public function testFeeOverweight(): void
{
$weight = 1501;
$expected = 4000 + (int) ceil(($weight - 1000) / 500) * 1000;
$this->assertSame($expected, (new ShippingCalculator())->fee($weight, 0));
}
ถ้าวันหนึ่งมีคนเปลี่ยน ceil เป็น floor ทั้งในโค้ดและในเทสต์ (หรือ AI แก้ทั้งสองที่พร้อมกัน) เทสต์ยังผ่าน และที่แย่กว่านั้นคือถ้าสูตรผิดตั้งแต่แรก เทสต์ก็แค่ยืนยันความผิดนั้น
เทสต์ที่ดีต้องใช้ ค่าคาดหวังที่คิดจากกติกาธุรกิจ เขียนเป็นตัวเลขตรง ๆ
public function testWeightJustOverOneKiloAddsOneStep(): void
{
// 1,001 กรัม = เกิน 1 กรัม ปัดเป็น 1 ขั้น → 40 + 10 = 50 บาท
$this->assertSame(5000, (new ShippingCalculator())->fee(1001, 0));
}
วิธีสั่ง AI ให้ได้เทสต์แบบนี้: ส่ง กติกาธุรกิจ เป็นข้อ ๆ ให้ AI และบอกชัดว่า "expected ต้องเป็นค่าคงที่ที่คำนวณด้วยมือจากกติกา ห้ามคำนวณด้วยสูตรในเทสต์" บางครั้งการไม่แนบโค้ด implementation เลยกลับได้เทสต์ที่ดีกว่า เพราะ AI ต้องคิดจาก spec ไม่ใช่จากโค้ด
หาเคส edge อย่างเป็นระบบ
ให้ AI ช่วยระดมเคส โดยบอกหมวดที่ต้องคิด
| หมวด | ตัวอย่างจาก ShippingCalculator |
|---|---|
| ค่าขอบพอดี | น้ำหนัก 1000 กับ 1001, ยอด 49999 กับ 50000 |
| ขอบของขั้นถัดไป | 1500 (ยังขั้นเดียว) กับ 1501 (สองขั้น) |
| ค่าต่ำสุด/ผิดกติกา | น้ำหนัก 1, 0, -5 |
| เงื่อนไขซ้อนกัน | พื้นที่ห่างไกล + ยอดถึงส่งฟรี (ควรฟรีหรือไม่ ต้องถามธุรกิจ) |
| ค่าขนาดใหญ่ | น้ำหนักหลายสิบกิโลกรัม |
สังเกตแถว "เงื่อนไขซ้อนกัน" การคิดเคส edge มักเผยว่า spec ไม่ชัด ในโค้ดปัจจุบัน พื้นที่ห่างไกลที่ยอดถึง 500 บาทจะส่งฟรี ซึ่งอาจไม่ใช่สิ่งที่ธุรกิจต้องการ นี่คือประโยชน์ของการเขียนเทสต์ที่ AI มักไม่บอกถ้าเราไม่ถาม ให้เพิ่มใน prompt ว่า "ถ้าพบกรณีที่ spec ไม่ชัด ให้ลิสต์เป็นคำถาม ไม่ต้องเดาคำตอบ"
Mutation-thinking: เทสต์จับบั๊กได้จริงไหม
Mutation testing คือการแก้โค้ดให้ผิดทีละจุดเล็ก ๆ (เรียกว่า mutant) แล้วดูว่าเทสต์แดงหรือไม่ ถ้าเทสต์ยังเขียว แปลว่าเทสต์ไม่ครอบคลุมจุดนั้น ใน PHP มีเครื่องมืออย่าง Infection ที่ทำแบบนี้อัตโนมัติ แต่ถึงไม่ใช้เครื่องมือ เราก็ใช้วิธีคิดนี้ได้ และให้ AI ช่วยได้ด้วย
นี่คือโค้ดและเทสต์ ลองลิสต์ mutation 10 แบบ เช่น เปลี่ยน
>=เป็น>,ceilเป็นfloor, ลบบรรทัดบวกค่าพื้นที่ห่างไกล แล้วบอกว่าเทสต์ชุดปัจจุบันจะจับ mutation ไหนไม่ได้ จากนั้นเสนอเทสต์ที่ต้องเพิ่ม
ตัวอย่าง mutation ที่เทสต์อ่อน ๆ มักพลาด
$subtotalSatang >= 50000เป็น>จับได้ก็ต่อเมื่อมีเทสต์ยอด 50000 พอดี$weightGrams > 1000เป็น>=จับได้ก็ต่อเมื่อมีเทสต์น้ำหนัก 1000 พอดีceilเป็นfloorจับได้ก็ต่อเมื่อมีน้ำหนักที่ไม่ลงตัว 500 เช่น 1001
หลังจากนั้นให้ลองทำ mutation เองด้วยมือสัก 2–3 จุด แล้วรันเทสต์ดูว่าแดงจริงไหม อย่าเชื่อการวิเคราะห์ของ AI โดยไม่ลอง
ตัวอย่างเทสต์ฉบับเต็ม
Codeception (unit suite ของ Yii2)
<?php
declare(strict_types=1);
namespace common\tests\unit\services;
use Codeception\Test\Unit;
use common\services\ShippingCalculator;
use InvalidArgumentException;
final class ShippingCalculatorTest extends Unit
{
private ShippingCalculator $calc;
protected function _before(): void
{
$this->calc = new ShippingCalculator();
}
public function testExactlyOneKiloIsBaseFee(): void
{
$this->assertSame(4000, $this->calc->fee(1000, 0));
}
public function testOneGramOverAddsOneStep(): void
{
$this->assertSame(5000, $this->calc->fee(1001, 0));
}
public function testFifteenHundredStillOneStep(): void
{
$this->assertSame(5000, $this->calc->fee(1500, 0));
}
public function testFifteenHundredOneIsTwoSteps(): void
{
$this->assertSame(6000, $this->calc->fee(1501, 0));
}
public function testRemoteAreaAddsThirtyBaht(): void
{
$this->assertSame(7000, $this->calc->fee(500, 0, true));
}
public function testFreeShippingStartsAtExactlyFiveHundredBaht(): void
{
$this->assertSame(4000, $this->calc->fee(500, 49999));
$this->assertSame(0, $this->calc->fee(500, 50000));
}
public function testZeroWeightIsRejected(): void
{
$this->expectException(InvalidArgumentException::class);
$this->calc->fee(0, 0);
}
}
PHPUnit แบบ data provider
ถ้าใช้ PHPUnit เวอร์ชันที่รองรับ attribute (10 ขึ้นไป) เขียนแบบตารางได้กระชับขึ้น เวอร์ชันเก่าใช้ annotation @dataProvider แทน
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class ShippingCalculatorTest extends TestCase
{
public static function feeCases(): array
{
return [
'base fee at 1kg' => [1000, 0, false, 4000],
'one gram over' => [1001, 0, false, 5000],
'top of first step' => [1500, 0, false, 5000],
'start of second step' => [1501, 0, false, 6000],
'remote area' => [500, 0, true, 7000],
'just below free' => [500, 49999, false, 4000],
'free at threshold' => [500, 50000, false, 0],
];
}
#[DataProvider('feeCases')]
public function testFee(int $weight, int $subtotal, bool $remote, int $expected): void
{
$this->assertSame($expected, (new ShippingCalculator())->fee($weight, $subtotal, $remote));
}
}
Prompt สำหรับขอเทสต์ที่มีค่า
เขียน Codeception unit test สำหรับ
ShippingCalculator::fee()ตามกติกาธุรกิจด้านล่าง (ไม่ใช่ตามโค้ด)
- expected ทุกตัวเป็นค่าคงที่ที่คิดจากกติกา ห้ามคำนวณด้วยสูตรในเทสต์
- ครอบคลุมค่าขอบทุกเงื่อนไข ค่าผิดกติกา และเงื่อนไขที่ซ้อนกัน
- ชื่อเทสต์บอกพฤติกรรม ไม่ใช่ testFee1
- ห้าม mock คลาสที่กำลังทดสอบ
- ถ้า spec ไม่ชัดในกรณีไหน ให้ลิสต์เป็นคำถามแยกต่างหาก
สรุป
- เทสต์ที่ดีต้องแดงเมื่อโค้ดผิด การผ่านอย่างเดียวไม่ได้พิสูจน์อะไร
- expected ต้องมาจากกติกาธุรกิจเป็นค่าคงที่ ไม่ใช่สูตรที่คัดลอกจากโค้ด
- ให้ AI ระดมเคส edge ตามหมวด: ค่าขอบ ค่าผิดกติกา เงื่อนไขซ้อน ค่าขนาดใหญ่
- ใช้ mutation-thinking ถามว่า "ถ้าโค้ดผิดแบบนี้ เทสต์จะจับได้ไหม" แล้วลองจริง
- การเขียนเทสต์มักเผยว่า spec ไม่ชัด ให้ AI ลิสต์เป็นคำถามแทนการเดา
ลองทำ
เลือกฟังก์ชันหนึ่งตัวในโปรเจกต์ที่มีเงื่อนไขตัวเลข เช่น คำนวณส่วนลดหรือค่าธรรมเนียม
- เขียนกติกาธุรกิจของฟังก์ชันนั้นเป็นข้อ ๆ ด้วยตัวเอง
- ให้ AI เขียนเทสต์ด้วย prompt ด้านบน แล้วรันให้ผ่าน
- ทำ mutation ด้วยมือ 3 จุด เช่น เปลี่ยน
>=เป็น>แล้วรันเทสต์ทีละครั้ง นับว่าจับได้กี่จุด - เพิ่มเทสต์สำหรับ mutation ที่หลุด แล้วคืนโค้ดให้ถูกต้อง (
git restore) ก่อน commit
ความคิดเห็น (0)
ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ