Automatic Testing #1: Assertion

    มีเรื่องหนึ่งที่โปรแกรมเมอร์ส่วนใหญ่มักมองข้าม เห็นเป็นเรื่องไม่ค่อยสำคัญ ซึ่งถ้าหากมองข้ามเรื่องนี้ไป ผมว่ามันจะนำปัญหาใหญ่มาสู่ผู้เขียนโปรแกรม ปัญหาอย่างไร เรามาดูกัน

วงจรอุบาทว์

    Extreme Programming ออกแบบโดยโปรแกรมเมอร์ ผู้ออกแบบทราบดีว่าสิ่งที่โปรแกรมเมอร์ไม่ชอบที่สุดมี 2 สิ่งคือ การเขียนเอกสาร และ การทดสอบโปรแกรม ในเรื่องของเขียนเอกสารนั้น ผมเคยเขียนรายละเอียดแนวคิดของ XP ในบทความก่อนๆ แล้ว ถ้าจำไม่ได้หรือยังไม่ได้อ่าน เปิดไปดูได้ครับ

     ในบทนี้ เรามาดูกันเรื่องการทดสอบโปรแกรม ผู้ออกแบบ XP มองว่าก่อนการเขียนโปรแกรม โปรแกรมเมอร์มักจะตั้งเป้าหมายไว้ในใจว่าอยากได้อะไร แล้วลงมือเขียนโปรแกรม เมื่อเขียนเสร็จ ก็ทดสอบเฉพาะสิ่งที่ตัวเองตั้งเป้าหมายไว้ โดยปกติแล้วใช้เวลาไม่มาก เท่าที่ผมเห็นมา มักใช้เวลาเฉลี่ยแล้วไม่เกิน 10 นาที ซึ่งโปรแกรมเมอร์ก็รู้ว่า มันคงมีหลุดบ้าง แต่เนื่องด้วยสาเหตุหลัก 2 ประการที่ทำให้โปรแกรมเมอร์ไม่ค่อยสนใจในการทดสอบโปรแกรมก็คือ มันน่าเบื่อเลยไม่อยากจะทำ และอีกข้อก็คือไม่มีเวลาทดสอบมีงานค้างอยู่เต็มมือไปหมด ซึ่งไม่ว่าจะประการไหนก็ตาม มันจะเกิดสิ่งที่เรียกว่าวงจรอุบาทว์ขึ้น อุบาทว์อย่างไรหรือครับ เรามาดูกัน

     เมื่อเราเขียนโปรแกรม แต่เราละเลยการทดสอบโปรแกรม ผลที่ตามมานั่นก็คือโปรแกรมที่ไม่สมบูรณ์ มันจะไม่ส่งผลออกมาเมื่อส่งมอบมากนัก แต่เมื่อเวลาผ่านไป เมื่อคุณเริ่มเขียนโปรแกรมใหม่ มันจะกลับมาหลอกหลอนคุณ ให้คุณต้องแก้ มันจะเข้าไปขัดจังหวะเวลาที่คุณเขียนโปรแกรมใหม่ ทำให้โปรแกรมใหม่คุณต้องล่าช้าไปอีก ผลก็คือโปรแกรมใหม่คุณก็ไม่มีเวลาทดสอบ เพราะมันใช้เวลาเลยแผนไปอีก คุณก็ต้องตาลีตาเหลือกไปทำงานที่ใหม่กว่า เกิดเป็นวงจรอุบาทว์ จนในที่สุดระบบงานของคุณจะหนืดมาก งานใหม่แทบไม่ได้เกิด เพราะมัวแต่ทำงานเก่า ทางออกของบางบริษัทก็คือ เมื่อเขียนโปรแกรมเสร็จหนึ่งระบบ ก็จะมีคนที่ทำหน้าที่ "ภารโรง" ห้อยทิ้งเอาไว้ อาจจะ 1 หรือ 2 คน ที่เหลือก็เดินทางผจญภัยไปกับระบบใหม่ ถ้าใช้ระบบนี้ไปเรื่อยๆ จำนวน "ภารโรง" จะมากขึ้นมากขึ้น และ "ภารโรง" ก็ทำงานอย่างซังกะตายไปทุกวัน เพราะไม่มีอะไรแปลกใหม่เข้ามาในชีวิต

    แต่การทำทดสอบโดยใช้แรงงานคนนั้น เป็นเรื่องที่ต้องใช้ทรัพยากรมาก และโอกาสหลุดมีสูง ดังนั้นนักวิทยาศาสตร์คอมพิวเตอร์ จึงหาวิธีต่างๆ นาๆ เพื่อมาช่วย โดยเฉพาะอย่างยิ่งให้ Code สามารถดูแลตัวเองได้ ซึ่งแนวคิดนี้เป็นแนวคิดหลักของ XP เลยทีเดียว

    แต่ใช่ว่า XP จะคิดค้นเป็นผู้คิดค้นวิธีทดสอบโปรแกรมขึ้นมาเอง ผู้ออกแบบ XP ก็กล่าวชัดว่า เขาไม่ได้คิดค้นอะไรใหม่เลย เพียงแต่เอาสิ่งดีๆ ที่คนอื่นใช้ เอามารวมรวมเข้าไว้ด้วยกัน เพื่อให้เข้าใจ ผมยังไม่พูดถึงการทดสอบแบบ XP ตรงๆ ตอนนี้ ผมอยากปูพื้นฐานก่อนว่า รากแท้จริงแล้ว XP ได้แนวคิดในการทดสอบมาจากไหน เราจะได้เข้าใจแนวคิดของ XP จริงๆ ครับ ดังนั้นในบทนี้ ผมขอคุยถึงรากฐานตัวหนึ่งที่ XP ยืมมา นั่นก็คือ Assertion

Assertion

    ในยุค Top-Down Programming นักวิทยาศาสตร์คอมพิวเตอร์ เสนอแนวคิดในการแก้วงจรอุบาทว์ นอกจากการทดสอบโปรแกรมให้หนักแล้ว ยังเสนอให้แทรก Code ทดสอบเข้าไปในโปรแกรม เพื่อว่าเมื่อเกิดปัญหาขึ้น มันจะไม่ลุกลามไปส่วนอื่นจนหาต้นตอไม่เจอ แทรก Code แทรกอย่างไร เรามาลองดูตัวอย่างกันครับ

public void calcGrade(string grade)
{
   
switch(grade)
    {
   
case "1": Console.WriteLine("Fail!"); break;
   
case "4": Console.WriteLine("Get Certificate"); break;
   
default: Console.WriteLine("Pass"); break;
    }
}

      ถ้า grade เป็น 1 ก็สอบตก grade 2, 3 ก็สอบผ่าน  แต่ถ้าได้ 4 จะได้รับใบประกาศ ทุกอย่างดูปกติดี ทำงานได้ ใช้งานมาร้อยครั้งไม่มีปัญหา แต่ด้วยความบังเอิญหรือไม่เข้าใจอย่างไรก็ตามแต่ โปรแกรมเมอร์อีกคน มาใช้ Code ส่วนนี้ กลับคิดว่า grade น่าจะเป็น A B C D F  ก็เลยมาเรียกใช้ฟังก์ชันนี้ ก็เกิดเรื่องสิครับ

    มีคนมาเข้าสอบคนหนึ่ง เวลาเรียนก็ไม่ตั้งใจเรียน เวลาสอบ กาข้อ ก. ตลอด ผลสอบออกมาจึงได้ F ไปตามระเบียบ โปรแกรมที่โปรแกรมเมอร์ใหม่คำนวณผลสอบออกมาได้ F แต่เมื่อเวลาส่งไปที่ฟังก์ชันข้างบนนี้ เขากลับสอบผ่านได้ใบประกาศเฉยเลย (จริงๆ แล้ว grade ไม่ควรเป็น string ตั้งแต่แรก แต่กรณีนี้ผมตั้งใจครับ จะได้มีบั๊กง่ายๆ หน่อย)

    เชื่อว่าคนที่เขียนโปรแกรมมานานๆ คงเคยประสบเหตุการณ์ทำนองนี้ ทางแก้ที่ดีทางหนึ่ง คือควรระบุ Case ให้ชัดไปเลยทุก Case ส่วน default นั้นใช้เฉพาะเวลาที่ต้องการดัก Error หรือถ้าจะให้ยิ่งดี ก็ควรจะดักตั้งแต่แรก โปรแกรมจะกลายเป็นดังนี้ครับ

public void calcGrade(string grade)
{
   
switch(grade)
    {
   
case "1": Console.WriteLine("Fail!"); break;
   
case "2": Console.WriteLine("Pass"); break;
   
case "3": Console.WriteLine("Pass"); break;
   
case "4": Console.WriteLine("Get Certificate"); break;
   
default:
       
Console.WriteLine("Error: in calcGrade invalid grade=" + grade);
        Environment.Exit(
1);
       
break;
    }
}

หรือ

public void calcGrade(string grade)
{
    if ( !(grade == "1" || grade == "2" || grade == "3" || grade == "4") ) {
       
Console.WriteLine("Error: in calcGrade invalid grade=" + grade);
        Environment.Exit(
1);
    }
   
switch(grade)
    {
   
case "1": Console.WriteLine("Fail!"); break;
   
case "2": Console.WriteLine("Pass"); break;
   
case "3": Console.WriteLine("Pass"); break;
   
case "4": Console.WriteLine("Get Certificate"); break;
    }
}

 

    ทั้ง 2 แบบก็พอใช้ได้ แต่การเขียนลักษณะนี้มีปัญหาอยู่สองประการครับ อย่างแรกก็คือ Code จะรกรุงรังมาก โดยปกติต่อการตรวจสอบ 1 ครั้ง เราต้องเสียบรรทัด ประมาณ 4 บรรทัดเมื่อใช้คำสั่ง if ในการตรวจสอบ ทำให้โปรแกรมอ่านยาก ปัญหาที่ 2 ก็คือ ในบางครั้งการตรวจสอบลักษณะนี้ถ้ามันตรวจสอบซับซ้อนขึ้น มันอาจใช้เวลามาก ยิ่งถ้าเราแทรกตัวตรวจสอบไว้ใน loop แล้วละก็ โปรแกรมจะอืดมาก ดังนั้นนักวิทยาศาสตร์คอมพิวเตอร์ ก็ได้คิดค้นคำสั่ง Assert ขึ้น ผมไม่รู้ว่าใครเป็นคนคิด แต่ผมเห็นตั้งแต่ภาษา C แล้ว มาลองดูครับว่าใน C# เราใช้กันอย่างไร

using System.Diagnostics;
public
void calcGrade(string grade
)
{

    Debug.Assert(grade == "1" || grade == "2" || grade == "3" || grade == "4", "in calcGrade invalid grade=" + grade);

    
switch(grade)
    {
   
case "1": Console.WriteLine("Fail!"); break;
   
case "2": Console.WriteLine("Pass"); break;
   
case "3": Console.WriteLine("Pass"); break;
   
case "4": Console.WriteLine("Get Certificate"); break;
    }
    Console.WriteLine("done");
}

       เมื่อโปรแกรมทำงานมาถึงคำสั่ง Debug.Assert() ค่า expression จะถูก evaluate ถ้าเงื่อนไขเป็นจริง ก็ไม่มีปัญหาอะไร ในกรณี ถ้า grade อยู่ใน 1-4 ก็ถือว่าปกติ แต่ถ้าไม่ใช่ เช่นเราใส่ "F" Debug.Assert() จะ Popup Windows ออกมาที่มี Call Stack ด้วยดังนี้ครับ

    เมื่อใดก็ตามที่ Debug.WriteIf() ทำงาน กล่าวคือเงื่อนไขเป็นจริง มันจะแสดงผลแล้วก็หยุดโปรแกรมทันที คุณไม่จำเป็นต้อง Exit() เอง และข้อสำคัญ มันจะ Popup Stack Frame ขึ้นมาดังนี้ครับ

 

    บางคนอาจจะกลัว Error ตัวนี้ แต่ถ้าคุณเป็นโปรแกรมเมอร์ต้องไม่กลัวมันครับ มันเป็นสิ่งดีถ้าเรามองในบางมุม เราสามารถศึกษาดูได้ว่ามีอะไรผิดพลาดเกิดขึ้น มี ข้อความที่เราเขียนขึ้นมาว่า มีการรับ grade เป็น F และมี StackFrame ให้รู้ว่า มีการเรียกเป็นชั้นๆ ได้อย่างไร ซึ่งช่วยในการแก้ไขโปรแกรมได้เป็นอย่างดี แต่เดี๋ยวก่อนนะครับ ถ้าคุณคอมไพล์โปรแกรมด้วย

csc class1.c

คุณจะไม่เห็น Error ตัวนี้ แถมยังโปรแกรมยังแสดงคำว่า "Done" อีกด้วย ราวกับว่ามันไม่มีการตรวจสอบ เลย ทำไมถึงเป็นเช่นนั้นเดี๋ยวเรามาคุยกัน แต่ตอนนี้ ถ้าลองใช้คำสั่งข้างล่าง จะได้ MessageBox ข้างต้นครับ

csc /d:DEBUG class1.c

    เราจะเห็นได้ว่าด้วยวิธีนี้ มีข้อดีแน่ๆ ก็คือ บรรทัดที่เขียนแทนที่ใช้ 4 บรรทัด กลับใช้เพียงแค่บรรทัดเดียว แถมแสดงเป็น MessageBox สวยงาม มี Stack Frame ให้ดูอีกต่างหาก ส่วนเรื่องที่บอกว่าพอเอาไปใช้งานแล้วมัวแต่ evaluate ค่าใน Assert() ทาง Microsoft ก็มีทางออกให้ครับ โดยที่ เขาแบ่ง Version ของโปรแกรมออกเป็น 2 Versions คือ Debug และ Release ถ้าอยู่ใน Version Debug นั่นก็คือกำลังพัฒนาอยู่ ตัวของ Assert() จะ Popup MessageBox และหยุดโปรแกรมตามที่ควรจะเป็น แต่ถ้าคอมไพล์ใหม่เป็น Version Release มันจะไม่แสดง MessageBox ครับ ไม่ใช่ไม่แสดงเฉยๆ มันไม่มีการทำเอาเลย เมื่อคอมไพเลอร์แปลไปถึงบรรทัดที่มีการเรียกใช้ Class Debug มันจะข้ามไปเลยไม่มีการแปล ทำให้ Code มันเบาลง แต่มันจะไม่มีการตรวจจับ Error นะครับ มันปล่อยผ่านไปเลย ดังนั้นต่อให้คุณส่ง "F" มามันก็จะแสดงคำว่า "Done" ที่เราดักเป็น  WriteLine() ไว้ข้างล่าง

    สำหรับ Version Debug ถ้าคุณคอมไพล์โดยใช้ csc.exe คุณสามารถใช้ ส่งค่า /d:DEBUG ถ้าไม่ส่งไปก็ถือว่าเป็น Version Release ครับ แต่ถ้าใช้ Visual Studio .NET มันมี Combo Box ตัวหนึ่งอยู่ใต้ Menu Bar ให้เลือกระหว่าง Debug กับ Release ดังรูปครับ

ทิ้งท้าย

    Assertion เป็นวิธีเก่าๆ ที่คิดค้นกันมานานแล้ว แต่ก็ยังใช้กันอยู่ โดยเฉพาะอย่างยิ่งโปรแกรมที่เขียนโดยใช้ภาษา C หรือ C++ ภาษา Java เอง ใน Version ใหม่ล่าสุด (J2SE 1.4) ก็เพิ่มการ assert เป็น keyword ตัวใหม่ตัวหนึ่งเลยทีเดียว แต่การใช้ Assertion เราต้องคำนึงอยู่เรื่องหนึ่ง นั่นก็คือ โปรแกรมที่เขียนนั้นไม่มีวันเสร็จ ถ้าเสร็จนั่นหมายถึงว่ามันตาย ไม่มีใครใช้อีกต่อไป ดังนั้น การที่จะออก Version Release ออกไป โดยการตัด Assertion ทิ้งทั้งหมด อาจไม่ใช่ทางเลือกที่ดี เอาอย่างนี้ดีกว่าครับ ถ้ามันไม่ช้าจนยอมรับไม่ ก็ปล่อย Debug Version ออกไปเถอะครับ อนาคตสดใสกว่าเยอะเลย

Supoj

15/12/2003