ช่วงที่ผ่านมาเราย้ายโปรแกรมคำนวณงบการเงินตัวหนึ่ง จากโค้ดเดิมไปเขียนใหม่อีกภาษา เป็นโปรแกรมที่รับผังบัญชีกับยอดทดลองเข้าไป แล้วคำนวณออกมาเป็นงบการเงิน พอเขียนของใหม่เสร็จก็เขียนเทสต์คุมไว้ รันแล้วผ่านหมดทุกตัว ไม่ตกสักตัว
แต่พอนั่งดูดี ๆ ก็สะดุด เทสต์พวกนั้นเราเป็นคนเขียนเอง ด้วยความเข้าใจโจทย์ชุดเดียวกับตอนเขียนโค้ด ถ้าเราเข้าใจอะไรผิดตั้งแต่ต้น เทสต์ก็จะผิดตามในทางเดียวกัน แล้วก็ยังผ่านอยู่ดี เทสต์ที่ผ่านจึงไม่ได้พิสูจน์ว่าของใหม่ทำงานเหมือนของเดิม มันพิสูจน์แค่ว่าของใหม่ตรงกับสิ่งที่เราคิดว่ามันควรทำ
เส้นแบ่งตรงนี้สำคัญมากตอนพอร์ตโค้ด เพราะงานพอร์ตต่างจากเขียนของใหม่ ตรงที่รู้อยู่แล้วว่าผลที่ถูกต้องคืออะไร นั่นคือของเดิมที่รันจริงมาก่อน วิธีที่เรียกว่า characterization testing ใช้ประโยชน์จากตรงนี้พอดี บทความนี้เล่าเป็นขั้น เริ่มจากมันคืออะไร ต่อด้วยวิธีพิสูจน์การพอร์ตด้วยการรันคู่ แล้วปิดที่กฎข้อหนึ่งที่ขัดสามัญสำนึก คือเจอบั๊กในโค้ดเดิม ให้ทำให้เหมือนไว้ก่อน อย่าเพิ่งแก้
ช่วงที่ 1characterization testing คืออะไร
characterization testing แปลตรงตัวคือการเทสต์เพื่อ "บันทึกลักษณะ" ของโค้ด แทนที่จะเริ่มจากถามว่าโค้ดควรทำอะไร มันเริ่มจากถามว่าโค้ดทำอะไรอยู่จริงตอนนี้ แล้วล็อกคำตอบนั้นไว้เป็นคำเฉลย จากนั้นทุกครั้งที่แก้โค้ด ก็เทียบกับคำเฉลยว่ายังเหมือนเดิมไหม เป็นชื่อที่ Michael Feathers ตั้งไว้ในหนังสือ Working Effectively with Legacy Code สำหรับงานรื้อโค้ดเก่าที่ยังไม่มีเทสต์
เทคนิคตระกูลนี้มีหลายชื่อ snapshot testing ที่คนทำหน้าเว็บคุ้น approval testing และ golden master ทั้งหมดหลักเดียวกัน คือเก็บผลลัพธ์ชุดหนึ่งไว้ตายตัว แล้วใช้มันเป็นตัวตัดสินของครั้งต่อไป ต่างกันแค่บริบทและเครื่องมือ
ทำไมมันเหมาะกับการพอร์ตเป็นพิเศษ
งานพอร์ตต่างจากเขียนของใหม่ตรงที่ รู้อยู่แล้วว่าผลที่ถูกต้องคืออะไร เพราะของเดิมที่รันจริงมาก่อนคือคำเฉลย ถ้าของใหม่ให้ผลตรงกับของเดิมทุกกรณี ก็มั่นใจได้ว่าพอร์ตมาครบ ไม่ใช่แค่ตรงกับที่เราเดาว่ามันควรทำ
ย้อนกลับไปที่ปัญหาตอนต้น เทสต์ที่เขียนเองมีจุดบอดตรงนี้พอดี ถ้าคนเขียนเทสต์กับคนเขียนโค้ดเข้าใจโจทย์มาแบบเดียวกัน ความเข้าใจที่ผิดจะติดไปทั้งสองฝั่งเท่ากัน เทสต์เลยยืนยันได้แค่ว่าโค้ดตรงกับความเข้าใจของเรา ไม่ได้ยืนยันว่าความเข้าใจนั้นถูก characterization testing ข้ามจุดบอดนี้ด้วยการเอาของเดิมมาเป็นคำเฉลย แทนความเข้าใจของเราเอง
ช่วงที่ 2พิสูจน์การพอร์ตด้วยการรันคู่ แล้วล็อกผลไว้เป็นคำเฉลย
วิธีที่พิสูจน์ได้แน่นที่สุดมีสามจังหวะ
- รันคู่ เอาของเดิมกับของใหม่มารันด้วยชุดข้อมูลเดียวกัน เก็บผลทั้งสองไว้
- เทียบทีละบรรทัดแบบ byte-diff ไม่ใช่ดูรวม ๆ ว่าใกล้กัน ตัวเลขต้องตรงทั้งตำแหน่งและค่า ถ้าต่างแม้บรรทัดเดียว แปลว่าพอร์ตยังไม่ตรง ต้องไล่ให้รู้ว่าทำไม
- ล็อกไว้เป็นคำเฉลย พอผลตรงกันหมดแล้ว เก็บผลของเครื่องต้นทางไว้เป็นไฟล์คำเฉลยตายตัว ไฟล์แบบนี้ในวงการเรียกว่า golden จากนั้นเทสต์ก็ไม่ต้องมีเครื่องเดิมอยู่ด้วยแล้ว แค่รันของใหม่ แล้วเทียบกับไฟล์คำเฉลยนี้
ยกจากงานจริง ตอนพอร์ตโปรแกรมคำนวณงบ เราเก็บผลของเครื่องต้นทางไว้เป็นไฟล์คำเฉลย (golden) แล้วตั้งชุดตรวจความตรงไว้ร้อยกว่าจุด ครอบทั้งงบแสดงฐานะการเงิน งบกำไรขาดทุน และงบกระแสเงินสด ทุกครั้งที่แก้โค้ดของใหม่ ชุดตรวจนี้จะเตือนทันทีถ้าผลเพี้ยนจากคำเฉลยแม้แต่บรรทัดเดียว โปรแกรมคำนวณจึงล็อกความถูกไว้ได้ โดยไม่ต้องแบกโค้ดเดิมไปทั้งก้อน
อย่าเชื่อข้อมูลจำลองที่ปั้นเอง
มีจุดที่คนมองข้ามบ่อยตอนเขียนเทสต์ อย่าเชื่อ mock หรือข้อมูลจำลองที่เราปั้นขึ้นมาเทสต์เอง ให้ยึดรูปร่างข้อมูลจริงจากต้นทาง ในงานนี้ ข้อมูลจำลองที่เขียนไว้ตั้งชื่อช่องข้อมูลไว้ชุดหนึ่ง แต่โครงสร้างข้อมูลจริงที่เครื่องต้นทางคืนมาใช้ชื่อคนละชุด ถ้าเชื่อข้อมูลจำลอง เทสต์จะผ่าน แต่พอต่อของจริงจะพัง หลักสั้น ๆ คือ ข้อมูลจำลองโกหกได้ โครงสร้างข้อมูลจริงไม่โกหก
ช่วงที่ 3เจอบั๊กในโค้ดเดิม ทำให้เหมือนไว้ก่อน อย่าเพิ่งแก้
ระหว่างเทียบผล characterization testing มักเจอของแถมที่ไม่ได้ตั้งใจหา นั่นคือบั๊กในโค้ดเดิม เพราะพอต้องเทียบทุกบรรทัด เราเลยได้อ่านพฤติกรรมของเครื่องต้นทางละเอียดกว่าตอนใช้งานปกติ
ครั้งนี้เราเจอจุดหนึ่งในเครื่องต้นทาง เป็นการจัดกลุ่มบัญชีด้วยการเช็คคำในชื่อกลุ่ม โค้ดอยากได้เฉพาะกลุ่ม "หมุนเวียน" แต่ไปเช็คแบบถามว่ามีคำว่า "หมุนเวียน" อยู่ในชื่อไหม ปัญหาคือคำว่า "ไม่หมุนเวียน" ก็มี "หมุนเวียน" เป็นส่วนหนึ่งของคำ การเช็คแบบนั้นเลยดึงกลุ่มไม่หมุนเวียนเข้ามาด้วย ที่ดินอาคารอุปกรณ์ กับเงินให้กู้ยืมระยะยาว เลยเข้าไปปนกับเงินทุนหมุนเวียน
สัญชาตญาณแรกคืออยากแก้มันเดี๋ยวนั้น แต่ในงานพอร์ต การแก้บั๊กในโค้ดใหม่กลับเป็นสิ่งที่ไม่ควรทำ เพราะเป้าหมายของการพอร์ตคือให้ของใหม่ทำงานเหมือนของเดิมเป๊ะ ทั้งส่วนที่ถูกและส่วนที่ผิด ถ้าเราแอบแก้บั๊กในโค้ดใหม่ ผลจะไม่ตรงกับคำเฉลยของเดิมทันที แล้วเราจะแยกไม่ออกว่าที่ผลต่าง เป็นเพราะพอร์ตพลาด หรือเพราะเราตั้งใจแก้
สิ่งที่ทำคือ ทำให้ของใหม่ผิดตามของเดิมให้ตรงเป๊ะ แล้วเขียนคอมเมนต์กำกับไว้ว่านี่คือบั๊กที่รู้ตัว พร้อมส่งเรื่องกลับไปให้เจ้าของโค้ดต้นทางแก้ที่ต้นทาง พอต้นทางแก้เมื่อไหร่ ความต่างจะโผล่มาให้เห็นชัด ๆ ให้เราตามแก้ของใหม่ได้ถูกจุด
กฎข้อนี้ฟังดูขัดใจ แต่มันคือหัวใจของการพอร์ตอย่างซื่อสัตย์ ทำให้เหมือนของเดิมก่อน รวมทั้งส่วนที่ผิด แล้วค่อยแก้ทีหลังแบบตั้งใจ ไม่ใช่แอบแก้ระหว่างทางจนสุดท้ายแยกไม่ออกว่าอะไรเป็นอะไร
ช่วงที่ 4เอาไปใช้กับงานจริง
ใช้กับงานแบบไหนได้บ้าง
- พอร์ตข้ามภาษาหรือข้ามเฟรมเวิร์ก ที่ของใหม่ต้องให้ผลเท่าของเดิม
- รื้อโค้ดเก่าที่ไม่มีเทสต์ ก่อนจะกล้าแก้ ต้องล็อกพฤติกรรมปัจจุบันไว้ก่อน
- เปลี่ยนไลบรารีหรือเครื่องข้างใน ที่ควรให้ผลเท่าเดิมหลังเปลี่ยน
- คุมผลของงานที่คำนวณเยอะ เช่น รายงานการเงิน ที่ค่าต้องตรงเป๊ะทุกบรรทัด
กฎข้อเดียวที่ต้องจำ
ถ้าจะจำอย่างเดียวจากบทความนี้ ขอให้เป็นข้อนี้ ตอนพอร์ตของ อย่าเชื่อเทสต์ที่ตัวเองเพิ่งเขียน ให้เชื่อของเดิมที่รันจริง เอาผลของมันมาเป็นคำเฉลย แล้วเทียบให้ตรงทีละบรรทัด เทสต์ที่เขียนจากความเข้าใจของเราเองคุ้มค่าน้อยที่สุดในจังหวะที่ยังไม่รู้ว่าความเข้าใจนั้นถูกหรือเปล่า
เริ่มยังไงดี
ไม่ต้องวางระบบใหญ่ตั้งแต่แรก ลองกับโค้ดก้อนเดียวก่อน
- เลือกโค้ดก้อนที่กำลังจะพอร์ตหรือรื้อ ที่ยังมีของเดิมรันได้อยู่
- เตรียมชุดข้อมูลตัวอย่างที่ครอบคลุมกรณีสำคัญ รันผ่านของเดิม เก็บผลไว้เป็นคำเฉลย (golden)
- รันของใหม่ด้วยชุดเดียวกัน เทียบกับคำเฉลยทีละบรรทัด
- ต่างตรงไหน ไล่ให้รู้ว่าเป็นเพราะพอร์ตพลาด หรือเจอบั๊กของเดิม ถ้าเป็นบั๊กเดิม ทำให้เหมือนไว้ก่อนพร้อมคอมเมนต์
- พอตรงกันหมด ล็อกคำเฉลยไว้เป็นเทสต์ถาวร ของใหม่แก้เมื่อไหร่ มันจะเตือนทันทีถ้าผลเพี้ยน
- อย่าให้ LLM คิดเลข แยกเครื่องคิดที่ต้องเป๊ะออกจากส่วนที่ใช้ดุลยพินิจ ทำไมงานที่ต้องแม่นควรเป็นโค้ด ไม่ใช่โมเดล
- เครื่องมือที่เอาไว้จับผิด ดันผิดเสียเอง วิธีทดสอบว่าเทสต์ของเราจับของพังได้จริงไหม
- เขียนจากงานจริง พอร์ตโปรแกรมคำนวณงบการเงินไปเป็นสคริปต์ในสกิล แล้วคุมความตรงด้วยชุดคำเฉลย (golden test) ร้อยกว่าจุด เทียบกับผลของเครื่องต้นทางแบบ byte-diff
- คำว่า characterization test มาจาก Michael Feathers, Working Effectively with Legacy Code (2004) เทคนิคตระกูลเดียวกันได้แก่ golden master, approval testing และ snapshot testing
บทความนี้เป็นหนึ่งชั้นใน สถาปัตยกรรม AI agent ระดับ production ทั้ง 7 ชั้น