ข้ามไปยังเนื้อหาหลัก
ProgrammerThailand

AI & Vibe Coding

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

ให้ 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 ลิสต์เป็นคำถามแทนการเดา

ลองทำ

เลือกฟังก์ชันหนึ่งตัวในโปรเจกต์ที่มีเงื่อนไขตัวเลข เช่น คำนวณส่วนลดหรือค่าธรรมเนียม

  1. เขียนกติกาธุรกิจของฟังก์ชันนั้นเป็นข้อ ๆ ด้วยตัวเอง
  2. ให้ AI เขียนเทสต์ด้วย prompt ด้านบน แล้วรันให้ผ่าน
  3. ทำ mutation ด้วยมือ 3 จุด เช่น เปลี่ยน >= เป็น > แล้วรันเทสต์ทีละครั้ง นับว่าจับได้กี่จุด
  4. เพิ่มเทสต์สำหรับ mutation ที่หลุด แล้วคืนโค้ดให้ถูกต้อง (git restore) ก่อน commit
ติดปัญหาตอนทำตาม? ถามในชุมชน

ความคิดเห็น (0)

ยังไม่มีความคิดเห็น — เริ่มคุยเป็นคนแรก ถามสิ่งที่สงสัย หรือแชร์ประสบการณ์ของคุณ