Part of 10306103 หลักการเขียนโปรแกรมเบื้องต้น (Fundamentals of Programming)
บทที่ 11 การดีบักและการทดสอบระดับหน่วย
แผนบริหารการสอนประจำบทที่ 11
บทนี้ว่าด้วยการดีบัก (debugging) และการทดสอบระดับหน่วย (unit testing) ของภาษา C ซึ่งเป็นกระบวนการตรวจหาและแก้ไขข้อผิดพลาด พร้อมยืนยันความถูกต้องของโปรแกรมอย่างเป็นระบบ ต่อยอดจากฟังก์ชันในบทที่ 6 เงื่อนไข if ในบทที่ 4 และตัวเลือกคอมไพล์ -Wall ในบทที่ 1 โดยอธิบายการจำแนกชนิดข้อผิดพลาด (syntax/runtime/logic) การอ่านข้อความและหมายเลขบรรทัดจากคอมไพเลอร์ เทคนิคการดีบักด้วยการแทรก printf ไล่ค่า การเขียนการทดสอบระดับหน่วยที่เทียบผลคาดหวังกับผลจริง และการใช้ assert ตรวจเงื่อนไขที่ต้องเป็นจริงเสมอ เพื่อให้พัฒนาโปรแกรมที่ถูกต้องและเชื่อถือได้
| ผลลัพธ์ | ข้อความ | หลักฐาน | CLO |
|---|---|---|---|
| LLO11.1 | จำแนกชนิดข้อผิดพลาด (syntax/runtime/logic) และใช้เทคนิคดีบัก (อ่านข้อความคอมไพเลอร์, printf ไล่ค่า, ตัวเลือก -Wall) | งานข้อ 1 (ปฏิบัติการที่ 11 Lab A) | CLO2 |
| LLO11.2 | ออกแบบกรณีทดสอบและเขียนการทดสอบระดับหน่วยที่เทียบผลคาดหวังกับผลจริง | งานข้อ 2 (ปฏิบัติการที่ 11 Lab A) | CLO3 |
| LLO11.3 | ใช้ assert ตรวจเงื่อนไขที่ต้องเป็นจริง และแก้ไขโปรแกรมให้ผ่านการทดสอบ | งานข้อ 3 (ปฏิบัติการที่ 11 Lab B) | CLO3 |
| LLO11.4 | ปฏิบัติตามมาตรฐานการเขียนโค้ด (การจัดย่อหน้า การตั้งชื่อสื่อความหมาย การใส่คอมเมนต์) ในงานโค้ดของบทนี้ | โค้ดทุกข้อที่ส่งในบทนี้ | CLO5 |
CLO ที่เกี่ยวข้อง (ข้อความจากหลักสูตร):
- CLO2 อธิบายขั้นตอนการทำงาน (Logic) ของภาษาโปรแกรมและเลือกใช้โครงสร้างควบคุม (Control Structure) ให้สอดคล้องกับโจทย์ที่กำหนดได้
- CLO3 สามารถเขียนโค้ดได้ถูกต้องตามหลักไวยากรณ์ (Syntax) และมีประสิทธิภาพ (Efficiency)
บทที่ 11 เป็นจุดที่นำฟังก์ชันจากบทที่ 6 และเงื่อนไข if จากบทที่ 4 มาใช้ตรวจสอบความถูกต้องของโปรแกรมอย่างเป็นระบบ จึงเป็นสะพานเชื่อมไปสู่การวัดประสิทธิภาพและคุณภาพโค้ดในบทที่ 12 และโครงงานบูรณาการในบทที่ 15 ที่ต้องส่งมอบโปรแกรมที่ผ่านการทดสอบ งานทั้งสามข้อในบทนี้สะสมหลักฐานของ CLO2 และ CLO3 ต่อเนื่องจากบทก่อน งานของบทนี้อยู่ในหมวดงานท้ายบทตามแผนคะแนนที่ผู้สอนยืนยันไว้ ดูรายละเอียดในบันทึกสำหรับผู้สอนท้ายบท
วิธีสอน
- ทบทวนการออกแบบฟังก์ชันจากบทที่ 6 และเงื่อนไข
ifจากบทที่ 4 แล้วตั้งโจทย์ที่โปรแกรมคอมไพล์ผ่านแต่ให้ผลผิด เพื่อชี้ให้เห็นว่าการคอมไพล์ผ่านไม่ได้รับประกันความถูกต้อง และนำเข้าสู่ความจำเป็นของการดีบักและการทดสอบ - แนะนำการจำแนกชนิดข้อผิดพลาดสามชนิดคือ syntax, runtime และ logic error พร้อมสาธิตการอ่านข้อความและหมายเลขบรรทัดจากคอมไพเลอร์ และบทบาทของตัวเลือก
-Wallที่ทบทวนจากบทที่ 1 - สาธิตเทคนิคการดีบักด้วยการแทรก
printfไล่ค่าตัวแปรในแต่ละขั้น เพื่อหาจุดที่ค่าคลาดเคลื่อนจากที่คาดไว้อย่างเป็นระบบ - สาธิตการเขียนฟังก์ชัน
checkที่เทียบผลคาดหวัง (expected) กับผลจริง (actual) แล้วรายงาน PASS/FAIL และนับผล พร้อมอธิบายการออกแบบกรณีทดสอบให้ครอบคลุมค่าปกติ ค่าขอบ และค่าลบ - สาธิตการใช้
assertตรวจเงื่อนไขที่ต้องเป็นจริงเสมอ แล้วให้ลงมือปฏิบัติตามใบงาน โดยกำหนดจุดตรวจระหว่างทางเพื่อให้ข้อมูลป้อนกลับทันที
สื่อการเรียนการสอน
- เครื่องคอมพิวเตอร์ที่เข้าถึงแพลตฟอร์มการเรียนของรายวิชาสำหรับเขียน คอมไพล์ และรันโปรแกรมภาษา C (มาตรฐาน C11) บนเว็บ
- เอกสารประกอบบทที่ 11 และสไลด์สรุปชนิดข้อผิดพลาด การทดสอบระดับหน่วย และการใช้ assert
- ไฟล์โปรแกรมตัวอย่าง
ch11_unittest.cและch11_assert.c
การวัดผลและการประเมินผล
กำหนดส่ง: งานข้อ 1–3 ภายใน 23:59 น. ของวันก่อนวันปฏิบัติการในสัปดาห์ที่ 12 ส่วนชุดฝึกท้ายบทและแบบฝึกหัดวัดความเข้าใจของบทนี้ ภายใน 23:59 น. ของวันก่อนวันบรรยายในสัปดาห์ที่ 12 ดูวันที่จริงของตอนเรียนได้ในห้องเรียนออนไลน์ (มติผู้สอน 28 ต.ค. 2569)
| ข้อ | งาน | หลักฐาน | ผลลัพธ์ | CLO |
|---|---|---|---|---|
| 1 | รับโปรแกรมที่มีข้อผิดพลาด ระบุชนิด และแก้ไขด้วยการอ่านคำเตือน/printf ไล่ค่า | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO11.1 | CLO2 |
| 2 | เขียน test harness เทียบ expected กับ actual ของฟังก์ชันที่กำหนด รายงาน PASS/FAIL | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO11.2 | CLO3 |
| 3 | ใช้ assert ตรวจเงื่อนไขก่อนทำงาน และแก้โปรแกรมให้ผ่านทุกกรณีทดสอบ | ไฟล์ซอร์สโค้ด + ผลการรัน | LLO11.3 | CLO3 |
เงื่อนไขผ่าน: งานข้อ 2 ประเมินจากไฟล์เดียวกับงานข้อ 1 ที่ต่อยอดด้วยการเขียนกรณีทดสอบ ดังนั้นงานข้อ 1 ต้องระบุชนิดข้อผิดพลาดถูกและแก้ไขให้คอมไพล์/รันผ่านก่อนจึงจะประเมินข้อ 2 ได้
คะแนนใบงาน (10 คะแนน)
| ผลลัพธ์ | สิ่งที่ตรวจ | คะแนน |
|---|---|---|
| LLO11.1 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO11.2 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO11.3 | งานข้อที่ระบุผลลัพธ์นี้ | 3 |
| LLO11.4 | มาตรฐานโค้ด ตรวจจากโค้ดทุกข้อที่ส่ง | 1 |
ใบงานบทที่ 1–13 รวมกันคิดเป็น 15% ของคะแนนรายวิชา ชิ้นละเท่ากัน ส่งช้าหัก 10% ของคะแนนงานต่อสัปดาห์ที่ช้า สูงสุด 50% และรับงานถึงสิ้นสัปดาห์ที่ 15 งานที่ผู้สอนส่งกลับให้แก้นับเวลาจากการส่งครั้งแรก ดูคะแนนของตนเองได้ที่หน้า “คะแนนของฉัน” ในห้องเรียนออนไลน์ของรายวิชา
เกณฑ์ให้คะแนน
งานข้อ 1 ใช้เกณฑ์ผ่าน/ไม่ผ่าน:
| เกณฑ์ | ผ่าน | ยังไม่ผ่าน |
|---|---|---|
| งานข้อ 1 | ระบุชนิดข้อผิดพลาดถูกต้องและแก้ไขให้โปรแกรมคอมไพล์และรันได้ผลถูก | ระบุชนิดผิด หรือแก้ไขแล้วยังคอมไพล์/รันไม่ผ่าน |
งานข้อ 2 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ (checklist): กรณีทดสอบครอบคลุมค่าปกติ ค่าขอบ และค่าลบ, เทียบ expected กับ actual ไม่สลับลำดับ, รายงาน PASS/FAIL และนับผลถูกต้อง, จัดย่อหน้า 4 ช่องว่างและมีคอมเมนต์ส่วนหัว
งานข้อ 3 ใช้เกณฑ์ผ่าน/ไม่ผ่าน ร่วมกับรายการตรวจ: วาง assert ตรวจเงื่อนไขถูกจุด, รวม assert.h ครบ, โปรแกรมผ่านทุกกรณีทดสอบ, เป็นไปตามมาตรฐานโค้ด
เฉลยจะเปิดในชั้นเรียนหลังทุกคนทำเสร็จ
บทนำ
การเขียนโปรแกรมให้คอมไพล์ผ่านเป็นเพียงขั้นแรกเท่านั้น โปรแกรมที่คอมไพล์ผ่านยังอาจหยุดทำงานขณะรัน หรือให้ผลลัพธ์ที่ผิดจากที่ต้องการได้ การดีบัก (debugging) คือกระบวนการตรวจหาและแก้ไขข้อผิดพลาด ส่วนการทดสอบระดับหน่วย (unit testing) คือการยืนยันว่าแต่ละฟังก์ชันทำงานถูกต้องด้วยการเทียบผลจริงกับผลที่คาดหวังอย่างเป็นระบบ บทที่ 4 ได้แนะนำเงื่อนไข if และบทที่ 6 ได้แนะนำฟังก์ชันไปแล้ว บทนี้นำทั้งสองเรื่องมาใช้สร้างเครื่องมือตรวจสอบความถูกต้องของโปรแกรม ตั้งแต่การจำแนกชนิดข้อผิดพลาด การไล่ค่าด้วย printf การเขียนกรณีทดสอบ ไปจนถึงการใช้ assert
ทักษะเรื่องการดีบักและการทดสอบในบทนี้เป็นพื้นฐานของบทที่ตามมา การยืนยันความถูกต้องของโปรแกรมเป็นส่วนหนึ่งของคุณภาพโค้ดในบทที่ 12 และโครงงานบูรณาการในบทที่ 15 ต้องส่งมอบโปรแกรมที่ผ่านการทดสอบด้วยกรณีทดสอบที่ครอบคลุม
| สิ่งที่ได้จากบทนี้ | ใช้ต่อในบท |
|---|---|
| การจำแนกชนิดข้อผิดพลาด (syntax/runtime/logic) และการอ่านข้อความคอมไพเลอร์ | บท 12 (คุณภาพโค้ด), บท 15 (โครงงาน) |
| เทคนิคการดีบักด้วย printf ไล่ค่า | ทุกบทที่ต้องตรวจหาข้อผิดพลาด |
| การทดสอบระดับหน่วย เทียบ expected กับ actual และการใช้ assert | บท 12 (คุณภาพโค้ด), บท 15 (โครงงาน) |
ความรู้พื้นฐาน เครื่องมือ และรุ่นซอฟต์แวร์
บทนี้ต้องใช้คอมไพเลอร์ภาษา C และโปรแกรมแก้ไขข้อความหรือ IDE และต้องมีความรู้เรื่องฟังก์ชันและการคืนค่าจากบทที่ 6 เงื่อนไข if จากบทที่ 4 และตัวเลือกคอมไพล์ -Wall จากบทที่ 1 ตารางด้านล่างเป็นรุ่นเครื่องมือที่ใช้ทดสอบโค้ดตัวอย่างในบทนี้จริง
| เครื่องมือ | รุ่นที่ทดสอบ | หน้าที่ |
|---|---|---|
| คอมไพเลอร์ gcc | 13.3.0 (Ubuntu) | แปลซอร์สโค้ด .c เป็นไฟล์ปฏิบัติการ |
| มาตรฐานภาษา | C11 (-std=c11) | กำหนดไวยากรณ์ภาษา C ที่ใช้อ้างอิง |
โค้ดตัวอย่างในบทนี้ทดสอบด้วย gcc 13.3.0 บนระบบปฏิบัติการลินุกซ์ หากใช้คอมไพเลอร์หรือระบบปฏิบัติการอื่น ข้อความแจ้งเตือนของคอมไพเลอร์และผลการแสดงข้อความภาษาไทยอาจต่างกันเล็กน้อย แต่ซอร์สโค้ดภาษา C มาตรฐานใช้ได้เหมือนกัน
วิธีใช้บทเรียนและใบงานในหน้านี้
ทบทวน บทที่ 10 การเรียงลำดับและการค้นหา โดยเลือกกรณีข้อมูลว่าง ค่าซ้ำ หรือไม่พบที่เคยทำนายผิดมาเป็นโจทย์ บทนี้ฝึกเปลี่ยนสิ่งที่สังเกตให้เป็นข้อทดสอบและคำอธิบายสาเหตุ
ใบงานเป็นแบบคัดลงสมุดหรือเอกสารของตน ไม่ใช่ช่องส่งคำตอบบนเว็บไซต์ ใช้ลำดับ ทำซ้ำ → เก็บข้อเท็จจริง → ตั้งสมมติฐาน → ตรวจเฉพาะจุด → แก้ → รันกรณีเดิมและกรณีสำคัญซ้ำ
ข้อมูลและเกณฑ์คะแนนที่ใช้เป็นข้อมูลสมมติ ไม่ใช่ข้อมูลผู้เรียนจริง กิจกรรมเพิ่มไม่เปลี่ยน LLO11.1–11.3 งานส่ง หรือสัดส่วนคะแนน เวลาและเฉลยอยู่เฉพาะส่วนผู้สอน
โปรแกรมบางโหมดตั้งใจให้รายงาน FAIL เพื่อสาธิตว่าข้อทดสอบจับความผิดได้ ไม่แก้ expected ให้ตรงกับผลผิดเพื่อให้หน้าจอเป็น PASS และไม่รันการเข้าถึงนอกอาร์เรย์หรือหารด้วยศูนย์เพื่อเดาผลลัพธ์
11.1 ชนิดข้อผิดพลาดและการอ่านข้อความคอมไพเลอร์
ระดับที่ 1: ข้อผิดพลาดในโปรแกรมแบ่งได้เป็นสามชนิดหลัก ได้แก่ ข้อผิดพลาดทางไวยากรณ์ (syntax error) ที่ทำให้คอมไพล์ไม่ผ่าน ข้อผิดพลาดขณะรัน (runtime error) ที่เกิดระหว่างการทำงาน และข้อผิดพลาดเชิงตรรกะ (logic error) ที่โปรแกรมคอมไพล์และรันได้แต่ให้ผลลัพธ์ผิด
ระดับที่ 2: syntax error คือการเขียนผิดไวยากรณ์ของภาษา เช่น ลืมเครื่องหมายอัฒภาคหรือวงเล็บไม่ครบ คอมไพเลอร์จะหยุดและแจ้งข้อความพร้อมหมายเลขบรรทัด runtime error เกิดขณะโปรแกรมทำงาน เช่น การหารด้วยศูนย์หรือการเข้าถึงหน่วยความจำนอกขอบเขต ส่วน logic error คือโปรแกรมทำงานได้แต่ผลไม่ตรงกับที่ต้องการ การอ่านข้อความจากคอมไพเลอร์ควรเริ่มจากข้อความแรกและหมายเลขบรรทัดที่ระบุ เพราะข้อผิดพลาดที่ตามมามักเป็นผลต่อเนื่อง ตัวเลือก -Wall จากบทที่ 1 เปิดการแจ้งเตือน (warning) ที่ช่วยจับข้อผิดพลาดบางอย่างที่อาจกลายเป็น logic error ตั้งแต่ขั้นคอมไพล์ เช่น ตัวแปรที่ประกาศแต่ไม่ได้ใช้ หรือการใช้ค่าก่อนกำหนด
gcc -std=c11 -Wall program.c -o programระดับที่ 3: ในสามชนิดนี้ logic error มักหายากที่สุด เพราะคอมไพเลอร์ไม่แจ้งเตือนและโปรแกรมรันจบตามปกติ ตัวอย่างเช่น ฟังก์ชันหาค่าสัมบูรณ์ที่เขียนเงื่อนไขผิดเป็น x > 0 แทน x < 0 จะยังคอมไพล์ผ่านแต่คืนค่าผิดเมื่อรับจำนวนลบ การจับ logic error จึงต้องอาศัยการทดสอบด้วยกรณีทดสอบที่ครอบคลุม ไม่ใช่พึ่งคอมไพเลอร์เพียงอย่างเดียว การเปิด -Wall เสมอช่วยลดโอกาสที่ข้อผิดพลาดบางส่วนจะหลุดรอดไปเป็น logic error ได้
คำถามตรวจความเข้าใจ 11.1: ข้อผิดพลาดที่คอมไพล์ผ่านแต่ให้ผลลัพธ์ผิดเรียกว่าชนิดใด และเหตุใดจึงหายากกว่าข้อผิดพลาดทางไวยากรณ์
ใบงาน 1: เขียนบันทึกที่เพื่อนทำซ้ำได้
สมมติพบค่าเฉลี่ย 3.00 จากข้อมูล 2 และ 5 ทั้งที่คาด 3.50 ให้กรอกข้อมูลก่อนแก้โค้ด แยกสิ่งที่เห็นจริงจากคำอธิบายที่ยังเป็นสมมติฐาน
| สิ่งที่ต้องบันทึก | รายละเอียดของผู้เรียน |
|---|---|
| ไฟล์และคำสั่งคอมไพล์ พร้อมตัวเลือก | |
| ข้อมูลเข้าแบบที่คัดไปรันซ้ำได้ | |
| ผลที่คาดจากโจทย์และวิธีคำนวณอิสระ | |
| ผลจริงและสถานะจบโปรแกรม | |
| ส่วนโค้ดเล็กที่สุดที่ยังทำให้เห็นปัญหา | |
| สมมติฐานแรกและสิ่งที่จะตรวจเพื่อคัดออก |
คำว่า “ค่าเฉลี่ยผิด” ยังไม่เพียงพอ ต้องระบุจำนวนข้อมูลและค่าที่ป้อน การลดตัวอย่างเหลือสองค่าที่ผลหารไม่ลงตัวช่วยแยกปัญหาการหารจำนวนเต็มได้ดีกว่าชุดที่หารลงตัวทุกกรณี
ใบงาน 2: จำแนกอาการก่อนเลือกเครื่องมือ
| สิ่งที่พบ | ช่วงที่ตรวจพบ | สมมติฐานชนิดข้อผิดพลาด | หลักฐานที่จะเก็บต่อ |
|---|---|---|---|
| ขาดอัฒภาคแล้วคอมไพล์ไม่ผ่าน | |||
| คอมไพล์ผ่านแต่ 2 กับ 5 เฉลี่ยเป็น 3.00 | |||
| ข้อสมมติภายในล้มเหลวและ assert แจ้งตำแหน่ง | |||
| คอมไพเลอร์เตือน format ไม่ตรงชนิด |
คำเตือนเป็นข้อวินิจฉัยที่ต้องอ่าน ไม่ใช่ชื่อชนิดบั๊กตายตัว ส่วนการอ่านนอกอาร์เรย์เป็นพฤติกรรมไม่กำหนด อาจไม่แครชให้เห็น จึงไม่ใช้ “รันจบ” เป็นข้อยืนยันว่าปลอดภัย
เริ่มจากข้อวินิจฉัยแรกและตรวจบรรทัดก่อนหน้าด้วย เพราะตำแหน่งที่คอมไพเลอร์เริ่มอ่านต่อไม่ได้อาจอยู่หลังจุดที่ลืมเครื่องหมายจริง อย่าแก้ทุกบรรทัดตามจำนวนข้อความโดยไม่หาต้นเหตุ
11.2 เทคนิคการดีบัก
ระดับที่ 1: การดีบักคือการหาจุดที่ค่าหรือพฤติกรรมของโปรแกรมคลาดเคลื่อนจากที่คาดไว้ แล้วแก้ไขให้ถูกต้อง หัวใจคือการจำกัดขอบเขตว่าข้อผิดพลาดอยู่ที่ส่วนใดของโปรแกรม
ระดับที่ 2: เทคนิคพื้นฐานที่ใช้ได้ทันทีคือการแทรกคำสั่ง printf เพื่อไล่ค่าตัวแปร (printf debugging) โดยพิมพ์ค่าของตัวแปรที่สำคัญในแต่ละขั้น แล้วเทียบกับค่าที่คาดว่าควรเป็น เมื่อพบขั้นที่ค่าเริ่มผิด แสดงว่าข้อผิดพลาดอยู่ในช่วงนั้น อีกเทคนิคหนึ่งคือการแยกส่วนทดสอบทีละฟังก์ชัน โดยเรียกฟังก์ชันที่สงสัยด้วยค่าที่รู้ผลล่วงหน้า เพื่อยืนยันว่าฟังก์ชันนั้นทำงานถูกหรือไม่ก่อนตรวจส่วนอื่น
printf("[debug] i = %d, sum = %d\n", i, sum);ระดับที่ 3: การดีบักที่เป็นระบบเริ่มจากการตั้งสมมติฐานว่าข้อผิดพลาดน่าจะอยู่ที่ใด แล้วตรวจทีละขั้นเพื่อยืนยันหรือปฏิเสธสมมติฐานนั้น แทนการแก้โค้ดแบบสุ่ม การไล่ค่าด้วย printf ควรเลือกจุดพิมพ์ที่แบ่งโปรแกรมออกเป็นช่วง ๆ เพื่อจำกัดขอบเขตให้แคบลงอย่างรวดเร็ว เมื่อแก้เสร็จแล้วควรลบคำสั่ง printf ที่ใช้ดีบักออก เพื่อไม่ให้ผลการแสดงปะปนกับผลลัพธ์จริงของโปรแกรม
คำถามตรวจความเข้าใจ 11.2: เทคนิค printf ไล่ค่าช่วยจำกัดขอบเขตของข้อผิดพลาดได้อย่างไร และควรทำอย่างไรกับคำสั่ง printf เหล่านั้นเมื่อแก้ไขเสร็จ
ใบงาน 3: ตรวจสิ่งที่แยกสมมติฐานได้
| จุดตรวจของข้อมูล 2 กับ 5 | ค่าที่ควรเห็น | ค่าที่เห็นจริง | สมมติฐานใดถูกคัดออก |
|---|---|---|---|
| count ก่อนเข้าลูป | |||
| sum ก่อนเริ่ม | |||
| หลังเพิ่มสมาชิกแรก | |||
| หลังเพิ่มสมาชิกที่สอง | |||
| นิพจน์ก่อนส่งค่ากลับ |
ถ้าพบ count เท่ากับ 2 และ sum เท่ากับ 7 ให้ตรวจชนิดตัวถูกหารและตัวหารในนิพจน์ ไม่ใช่สรุปว่าตัวแปรรับผลเป็น double แล้วจะหารเป็นทศนิยมโดยอัตโนมัติ
เพิ่ม printf เฉพาะจุดที่ตอบคำถามได้ และบันทึกค่าก่อน/หลังให้ชัด เมื่อยืนยันสาเหตุแล้วเก็บกรณีทดสอบแทนข้อความดีบักที่รบกวนผลปกติของโปรแกรม
11.3 การทดสอบระดับหน่วย
ระดับที่ 1: การทดสอบระดับหน่วย (unit testing) คือการทดสอบฟังก์ชันทีละหน่วยด้วยกรณีทดสอบที่รู้ผลล่วงหน้า เพื่อยืนยันว่าฟังก์ชันให้ผลถูกต้องก่อนนำไปใช้ร่วมกับส่วนอื่น
ระดับที่ 2: รูปแบบที่ใช้บ่อยคือเขียนฟังก์ชันช่วยทดสอบ เช่น check ที่รับผลคาดหวัง (expected) กับผลจริง (actual) แล้วเทียบด้วยเงื่อนไข if จากบทที่ 4 ว่า expected == actual หรือไม่ หากตรงกันให้รายงาน PASS หากไม่ตรงให้รายงาน FAIL พร้อมแสดงค่าที่คาดหวังและค่าที่ได้ และนับจำนวนกรณีที่ผ่านกับจำนวนกรณีทั้งหมด การทดสอบจึงเทียบผลโดยอัตโนมัติแทนการตรวจด้วยสายตา
void check(const char *name, int expected, int actual) {
tests_run++;
if (expected == actual) {
tests_passed++;
printf("PASS: %s\n", name);
} else {
printf("FAIL: %s (คาดหวัง %d แต่ได้ %d)\n", name, expected, actual);
}
}ระดับที่ 3: กรณีทดสอบที่ดีควรครอบคลุมค่าหลายลักษณะ ได้แก่ ค่าปกติทั่วไป ค่าขอบ (boundary) เช่น ศูนย์หรือค่าสุดเขต และค่าลบ เพื่อเพิ่มโอกาสพบความผิดตามข้อกำหนด โดยไม่ถือว่ากรณีจำกัดพิสูจน์ข้อมูลทุกค่าที่อาจส่งมา ตัวอย่างเช่น การทดสอบฟังก์ชันหาค่าสัมบูรณ์ควรทดสอบทั้งค่าบวก ค่าลบ และศูนย์ กับดักที่พบบ่อยคือการสลับลำดับ expected กับ actual ซึ่งทำให้ข้อความรายงานคลาดเคลื่อน และการทดสอบเฉพาะค่าปกติโดยลืมค่าขอบและค่าลบ ซึ่งทำให้ logic error บางส่วนหลุดรอดไปได้
คำถามตรวจความเข้าใจ 11.3: กรณีทดสอบที่ดีควรครอบคลุมค่าใดบ้าง และการสลับลำดับระหว่าง expected กับ actual ส่งผลอย่างไร
ตัวอย่างพร้อมใช้: ข้อทดสอบต้องล้มเหลวเมื่อสูตรผิด
สร้างไฟล์ mean_regression.c ป้อน 0 เพื่อใช้สูตรแก้แล้ว หรือ 1 เพื่อใช้สูตรหารจำนวนเต็มที่ตั้งใจใส่ไว้เป็นตัวอย่างผิด กรณีทดสอบใช้ผลที่แทนได้ตรงและ expected คงที่จากการคำนวณด้วยมือ
/* mean_regression.c : the same checks distinguish good and bad logic */
#include <assert.h>
#include <stdio.h>
double mean_buggy(const int values[], int count);
double mean_fixed(const int values[], int count);
int main(void)
{
int mode;
if (scanf("%d", &mode) != 1) {
puts("Input error");
return 1;
}
if (mode != 0 && mode != 1) {
puts("Range error");
return 1;
}
const int cases[3][2] = {{2, 5}, {0, 0}, {-2, -5}};
const double expected[3] = {3.5, 0.0, -3.5};
int passed = 0;
for (int i = 0; i < 3; ++i) {
double actual = mode == 1 ? mean_buggy(cases[i], 2)
: mean_fixed(cases[i], 2);
if (actual == expected[i]) {
++passed;
printf("PASS case%d\n", i + 1);
} else {
printf("FAIL case%d expected=%.2f actual=%.2f\n",
i + 1, expected[i], actual);
}
}
printf("passed=%d total=3\n", passed);
return passed == 3 ? 0 : 1;
}
/* Deliberately wrong; count is positive and the small sum fits int. */
double mean_buggy(const int values[], int count)
{
assert(count > 0);
int sum = 0;
for (int i = 0; i < count; ++i) {
sum += values[i];
}
return sum / count;
}
/* Same contract, but conversion takes place before division. */
double mean_fixed(const int values[], int count)
{
assert(count > 0);
int sum = 0;
for (int i = 0; i < count; ++i) {
sum += values[i];
}
return (double)sum / count;
}ทำนายว่าโหมด 1 จะ FAIL กรณีใดบ้าง และเหตุใดกรณีศูนย์ยัง PASS ทั้งที่สูตรผิด โหมด 0 ต้องผ่านกรณีเดิมโดยไม่แก้ expected และเมื่อมี FAIL โปรแกรมคืนสถานะไม่เป็นศูนย์
การใช้ == ในตัวอย่างนี้เหมาะกับค่าที่เลือกอย่าง 3.5 และ 0 ไม่ใช่กฎทดสอบทศนิยมทุกโจทย์ การคำนวณอื่นอาจต้องกำหนดความคลาดเคลื่อนตามหน่วยและขนาด
ใบงาน 4: เลือกข้อมูลที่แยกฉบับผิดออกจากฉบับถูก
| ข้อกำหนด | ความผิดที่สงสัย | ข้อมูลที่แยกความผิดได้ | expected จากโจทย์ |
|---|---|---|---|
| ผ่านตั้งแต่คะแนน 50 | ใช้ score > 50 | ||
| เลือกค่าสูงสุดของค่าลบ | เริ่ม best ที่ 0 | ||
| ค้นหาแล้วอ่านค่าที่พบ | ตีความดัชนี 0 ว่าไม่พบ | ||
| เฉลี่ยข้อมูลสองค่า | หารจำนวนเต็มก่อนแปลง | ||
| อ่านสมาชิกครบชุด | ข้ามสมาชิกสุดท้าย |
ให้เพื่อนเสนอโปรแกรมฉบับผิดที่ยังผ่านชุดทดสอบเดิม แล้วเพิ่มกรณีที่จำเป็นเพียงกรณีเดียวก่อน ไม่เพิ่มข้อมูลจำนวนมากโดยยังไม่รู้ว่าต้องจับความผิดชนิดใด
expected ต้องมาจากข้อกำหนด ตัวอย่างมือ หรือวิธีอิสระ ไม่คัดสูตรผิดจาก actual ไปทำ expected ซ้ำ การแก้ผลคาดให้ตรงผลผิดจะทำให้การทดสอบสูญเสียหน้าที่
11.4 assert และการพัฒนาแบบมีการทดสอบ
ระดับที่ 1: assert คือเครื่องมือที่ใช้ตรวจเงื่อนไขที่ต้องเป็นจริงเสมอ ณ จุดหนึ่งของโปรแกรม หากเงื่อนไขเป็นเท็จ โปรแกรมจะหยุดทันทีพร้อมแจ้งตำแหน่งที่ผิด
ระดับที่ 2: การใช้ assert ต้องรวมส่วนหัว #include <assert.h> ก่อน แล้วเขียน assert(เงื่อนไข) ในจุดที่ต้องการตรวจ เช่น assert(b != 0) ก่อนการหารเพื่อยืนยันว่าตัวหารไม่เป็นศูนย์ หากเงื่อนไขเป็นจริง โปรแกรมทำงานต่อตามปกติ หากเป็นเท็จ โปรแกรมจะหยุดและแสดงข้อความว่าการตรวจสอบล้มเหลว (assertion failed) พร้อมชื่อไฟล์และหมายเลขบรรทัด ซึ่งช่วยระบุสาเหตุได้เร็ว
#include <assert.h>
int divide(int a, int b) {
assert(b != 0);
return a / b;
}ระดับที่ 3: assert ต่างจากการตรวจด้วย if และ printf ตรงที่ assert ใช้สำหรับเงื่อนไขที่ "ต้องเป็นจริงเสมอ" ตามตรรกะของโปรแกรม หากล้มเหลวแสดงว่าตรรกะผิดและต้องแก้ไขโค้ด ไม่ใช่ความผิดพลาดของข้อมูลนำเข้าที่ควรจัดการด้วยเงื่อนไขปกติ วงจรการพัฒนาแบบมีการทดสอบคือการเขียนกรณีทดสอบ รันเพื่อดูว่าผ่านหรือไม่ แล้วแก้โปรแกรมจนผ่านทุกกรณี วนซ้ำเช่นนี้จนมั่นใจในความถูกต้อง การใช้ assert ร่วมกับการทดสอบระดับหน่วยจึงเป็นส่วนสำคัญของการพัฒนาโปรแกรมที่มีคุณภาพ ซึ่งจะต่อยอดในบทที่ 12
คำถามตรวจความเข้าใจ 11.4: assert ต่างจากการตรวจด้วย if และ printf อย่างไร และเมื่อเงื่อนไขใน assert เป็นเท็จจะเกิดอะไรขึ้น
ข้อจำกัดของตัวอย่างค่าสัมบูรณ์: ฟังก์ชันที่คืน int ไม่สามารถแทนค่าสัมบูรณ์ของ INT_MIN บนระบบที่ค่าสูงสุดและต่ำสุดไม่สมมาตรได้ จึงต้องกำหนดขอบเขตข้อมูลหรือวิธีรายงานข้อผิดพลาดก่อนขยายการทดสอบ ไม่จงใจคำนวณ -INT_MIN เพื่อคาดหวังผลลัพธ์แบบวนกลับ
11.5 กรณีศึกษาต่อเนื่อง: ค่าเฉลี่ยผิดแม้โปรแกรมรันจบ
สมมติผู้ใช้รายงานว่าโปรแกรมคะแนนให้ค่าเฉลี่ย 3.00 สำหรับข้อมูล 2 และ 5 แทน 3.50 เริ่มจากเก็บข้อเท็จจริงที่ทำซ้ำได้ ได้แก่ข้อมูลเข้า ผลที่คาด ผลจริง และส่วนคำนวณที่เกี่ยวข้อง ไม่เริ่มด้วยการเปลี่ยนชนิดทุกตัวเป็น double เพราะอาจแก้เกินจุดโดยยังไม่รู้สาเหตุ
11.5.1 ตั้งสมมติฐานที่แยกกันได้
สมมติฐานแรกคือรับค่าไม่ครบ สมมติฐานที่สองคือผลรวมผิด และสมมติฐานที่สามคือหารแบบจำนวนเต็ม หากตรวจพบว่า count เป็น 2 และ sum เป็น 7 ก่อนหาร สองข้อแรกมีหลักฐานคัดออกได้ เหลือการตรวจชนิดในนิพจน์ sum / count ไม่ใช่ดูเพียงชนิดของตัวแปรรับผล
เพิ่มข้อความตรวจเฉพาะจุดชั่วคราว หรือใช้เครื่องมือดีบักที่แพลตฟอร์มรองรับ แล้วลบข้อความที่รบกวน output ก่อนส่งงาน เก็บความรู้ที่ได้ไว้ในกรณีทดสอบแทนการเก็บ printf กระจายไปทั่วโปรแกรม การดีบักที่ดีทำให้คำอธิบายสาเหตุแคบลงในแต่ละขั้น
11.5.2 เปลี่ยนข้อผิดพลาดหนึ่งครั้งเป็นการทดสอบถาวร
#include <assert.h>
#include <stdio.h>
double mean_score(const int values[], int count)
{
assert(count > 0);
int sum = 0;
for (int i = 0; i < count; ++i) {
sum += values[i];
}
return (double)sum / count;
}
int main(void)
{
const int a[] = {2, 5};
const int b[] = {0};
const int c[] = {100, 100};
double actual_a = mean_score(a, 2);
double actual_b = mean_score(b, 1);
double actual_c = mean_score(c, 2);
if (actual_a != 3.5 || actual_b != 0.0 || actual_c != 100.0) {
puts("Check failed");
return 1;
}
puts("3 checks passed");
return 0;
}กรณีนี้เลือกผลที่แทนได้ตรงอย่าง 3.5, 0 และ 100 จึงใช้การเท่ากันตรง ๆ ในตัวอย่างได้ แต่ไม่ได้หมายความว่าค่าทศนิยมทุกผลควรทดสอบด้วย == หากผลมาจากการคำนวณที่ปัดเศษ ต้องกำหนดค่าคลาดเคลื่อนที่เหมาะกับหน่วยและขนาด ไม่ใช้ค่า epsilon เดียวกับทุกโจทย์โดยไม่คิด
ฟังก์ชันสมมติว่าอาร์เรย์มีจำนวนสมาชิกพอและผลรวมไม่เกินช่วง int ข้อทดสอบสามกรณีพิสูจน์เพียงพฤติกรรมของข้อมูลเหล่านี้ ไม่ได้พิสูจน์ว่ารับข้อมูลใดก็ได้ หากต้องใช้คะแนนจำนวนมากต้องวิเคราะห์ขอบเขตผลรวมและชนิดอีกครั้ง
11.6 assert เป็นเครื่องมือตรวจข้อสมมติ ไม่ใช่แบบฟอร์มรับข้อมูล
11.6.1 ทำงานสำคัญนอก assert เสมอ
ไม่ควรเขียน assert(scanf("%d", &score) == 1); เพื่อรับข้อมูล เพราะเมื่อกำหนด NDEBUG นิพจน์ใน assert จะไม่ถูกประเมิน การรับข้อมูลอาจไม่เกิดเลย เขียนการอ่านจริงก่อน แล้วใช้ if จัดการข้อผิดพลาดตามปกติ ส่วน assert ใช้ตรวจความสัมพันธ์ภายในที่โปรแกรมเมอร์คาดว่าต้องจริง
เช่นเดียวกัน ไม่เพิ่มตัวนับภายใน assert(++count < limit) แล้วหวังให้ทุกโหมดได้สถานะเหมือนกัน การตรวจควรไม่มีผลข้างเคียงที่จำเป็นต่อโปรแกรม glibc: Consistency Checking อธิบายผลของ NDEBUG และเตือนไม่ให้นำผลข้างเคียงไปไว้ใน assertion
โปรแกรมค่าเฉลี่ยฉบับนี้เรียกฟังก์ชันและตรวจ expected ด้วย if ภายนอก assert จึงตรวจจริงแม้กำหนด NDEBUG ข้อความ 3 checks passed แสดงหลังตรวจครบแล้ว การบันทึกผลยังต้องรวมตัวเลือกคอมไพล์และสถานะจบ
11.6.2 คอมไพเลอร์ช่วยตรวจได้บางประเภท
การเปิดคำเตือนช่วยพบชนิดไม่ตรงกับ format ตัวแปรไม่ใช้ และรูปแบบที่น่าสงสัยบางอย่าง แต่ -Wall ไม่ได้เปิดคำเตือนทุกตัว และไม่มีคำเตือนก็ไม่ได้รับรองว่าสูตรหรือข้อกำหนดถูกทั้งหมด อ่านขอบเขตตัวเลือกจาก GCC: Warning Options
ถ้าเครื่องมือของผู้สอนรองรับ AddressSanitizer หรือ UndefinedBehaviorSanitizer อาจใช้ช่วยตรวจบางปัญหาระหว่างรัน ตัวเลือกและระบบที่รองรับแตกต่างกัน จึงเป็นเครื่องมือประกอบการตรวจของผู้สอน ไม่ใช่ข้อกำหนดให้ผู้เรียนติดตั้งเพิ่ม รายละเอียดอยู่ใน GCC: Instrumentation Options และไม่ควรสรุปว่าโปรแกรมไม่มีบั๊กเพราะ sanitizer ไม่รายงาน
ตัวอย่างข้อมูลนำเข้า: ตรวจด้วย if ก่อนใช้ assert ภายใน
สร้างไฟล์ guarded_divide.c รับ a และ b ช่วง -100 ถึง 100 และปฏิเสธ b เป็นศูนย์ด้วย if ตามปกติ ช่วงนี้ทำให้ผลหารจำนวนเต็มแทนได้ใน int และไม่เจอกรณีผลหารเกินช่วง
/* guarded_divide.c : input handling must survive NDEBUG */
#include <assert.h>
#include <stdio.h>
int divide_checked(int a, int b);
int main(void)
{
int a;
int b;
if (scanf("%d %d", &a, &b) != 2) {
puts("Input error");
return 1;
}
if (a < -100 || a > 100 || b < -100 || b > 100) {
puts("Range error");
return 1;
}
if (b == 0) {
puts("Zero divisor");
return 1;
}
printf("quotient=%d\n", divide_checked(a, b));
return 0;
}
/* Internal caller contract: bounded inputs and nonzero divisor. */
int divide_checked(int a, int b)
{
assert(b != 0);
return a / b;
}คอมไพล์ทั้งแบบปกติและเพิ่ม -DNDEBUG แล้วทดสอบ 10/2, -7/2 และ 10/0 ข้อมูลผิดต้องยังถูกปฏิเสธเมื่อปิด assertions เพราะงานอ่านและตรวจข้อมูลไม่ได้ซ่อนอยู่ใน assert
ตัวอย่าง divide แบบทั่วไปยังต้องกำหนดว่าผลหารแทนด้วย int ได้ ไม่ตรวจเพียงตัวหารไม่เป็นศูนย์แล้วสรุปว่าทุกคู่ปลอดภัย เช่น INT_MIN หาร -1 อาจเกินช่วงบนระบบที่ช่วงไม่สมมาตร
ตัวอย่างสองโหมด: ทำงานจำเป็นก่อนตรวจข้อสมมติ
สร้างไฟล์ assertion_modes.c สังเกตว่าฟังก์ชันคำนวณถูกเรียกหนึ่งครั้งทั้งสองโหมด ส่วน assert ตรวจเพียงค่าที่คำนวณไว้แล้ว การกำหนด NDEBUG ไม่ควรทำให้งานจำเป็นหายไป
/* assertion_modes.c : necessary work is outside assert */
#include <assert.h>
#include <stdio.h>
int calls = 0;
int compute(void)
{
++calls;
return 1;
}
int main(void)
{
int actual = compute();
assert(actual == 1);
printf("calls=%d actual=%d\n", calls, actual);
#ifdef NDEBUG
puts("assertions=off");
#else
puts("assertions=on");
#endif
return actual == 1 ? 0 : 1;
}gcc -std=c11 -Wall -Wextra -Wpedantic assertion_modes.c -o assertion_modes
gcc -std=c11 -Wall -Wextra -Wpedantic -DNDEBUG assertion_modes.c -o assertion_modes_release| โหมด | compute ถูกเรียกกี่ครั้ง | ค่าที่พิมพ์ | ข้อสมมติถูกประเมินหรือไม่ |
|---|---|---|---|
| ปกติ | |||
| มี NDEBUG ก่อนรวม assert.h |
อภิปรายว่าถ้าย้าย compute เข้า assert(compute() == 1) จะเกิดอะไรเมื่อปิด assertions การทดลองแบบนั้นมีผลข้างเคียงที่หายไปได้ จึงใช้เป็นตัวอย่างสิ่งที่ต้องหลีกเลี่ยง ไม่ใช้เป็นรูปแบบโปรแกรมที่ถูก
11.7 เวิร์กช็อปขยาย: แยกการทำซ้ำ การวินิจฉัย และ regression test
ใช้สำเนาโปรแกรมคะแนนที่มีข้อผิดพลาดแบ่งเป็นสามงาน งานแรกเปลี่ยน i < count เป็น i <= count ให้ผู้เรียนตรวจด้วยการอ่านขอบเขตและเครื่องมือที่มี ไม่จงใจพึ่งผลนอกอาร์เรย์เป็นคำตอบคาดหวัง งานที่สองเริ่มค่าสูงสุดที่ศูนย์แล้วทดสอบข้อมูลติดลบทั้งหมด งานที่สามล้างตัวสะสมผิดตำแหน่งแล้วใช้ข้อมูลสองค่าที่ต่างกัน
สำหรับแต่ละงาน เขียนบันทึกสั้นตามลำดับ ได้แก่ขั้นทำซ้ำ ข้อเท็จจริงที่เห็น สมมติฐาน จุดที่ตรวจพบสาเหตุ การแก้เฉพาะจุด และกรณีทดสอบที่ป้องกันการกลับมาผิดซ้ำ ถ้ามีการแก้หลายเรื่องพร้อมกัน ให้แยกเพื่อบอกได้ว่าการเปลี่ยนใดแก้สาเหตุจริง
เพิ่มชุดทดสอบการค้นหาจากบทที่ 10 ได้แก่พบดัชนีศูนย์ พบตัวท้าย ไม่พบ และชุดว่าง การตรวจเฉพาะ “พบค่า” ยังไม่ทดสอบว่าผู้เรียกจัดการ -1 ก่อนอ่านสมาชิกหรือไม่ จึงควรมีทั้งการทดสอบฟังก์ชันเดี่ยวและเส้นทางใช้งานที่เกี่ยวข้อง
ท้ายงานสลับบทบาทให้เพื่อนอ่านรายงานโดยไม่ดูโค้ดฉบับแก้ เพื่อนต้องทำซ้ำจากข้อมูลที่ให้ได้ หากยังต้องถามว่าใช้ค่าอะไรหรือคาดผลเท่าไร รายงานยังไม่เพียงพอ ความสามารถในการสื่อสารบั๊กเป็นส่วนหนึ่งของการพัฒนาโปรแกรมร่วมกับผู้อื่น ไม่ใช่งานเอกสารที่แยกจากการเขียนโค้ด
11.8 ตัวอย่างเปรียบเทียบ: ทดสอบที่จับบั๊กได้กับทดสอบที่เพียงรันผ่าน
สมมติฟังก์ชันนับคะแนนที่ผ่านเขียน score > 50 แต่ข้อกำหนดเป็นอย่างน้อย 50 ถ้าทดสอบด้วย 40 และ 80 จะได้คำตอบถูกทั้งฉบับผิดและฉบับถูก จึงยังไม่มีกรณีที่แยกความผิดนี้ออกมา คะแนน 50 เป็นกรณีสำคัญเพราะอยู่ตรงจุดที่สองนิพจน์ให้ผลต่างกัน
ให้สร้างตารางสามคอลัมน์ ได้แก่ข้อกำหนด ความผิดที่เป็นไปได้ และข้อมูลที่เปิดเผยความผิด ตัวอย่างข้อกำหนดค้นหาตัวแรกต้องรองรับดัชนี 0 ความผิดที่เป็นไปได้คือผู้เรียกใช้ค่าคืนเป็น boolean และข้อมูลที่เหมาะคือเป้าหมายอยู่สมาชิกแรก การเลือกกรณีจากสมมติฐานผิดช่วยให้ชุดทดสอบมีเหตุผลมากกว่าการเพิ่มข้อมูลสุ่มจำนวนมากอย่างเดียว
| ข้อผิดพลาดที่สงสัย | กรณีแยกความผิด | เหตุผล |
|---|---|---|
| ใช้มากกว่าแทนอย่างน้อย | คะแนน 50 | อยู่ตรงจุดเปลี่ยนเงื่อนไข |
| ลืมสมาชิกสุดท้าย | ค่าสูงสุดอยู่ท้าย | ถ้าไม่อ่านท้ายจะตอบผิด |
| เริ่มค่าสูงสุดที่ศูนย์ | ค่าทั้งหมดติดลบ | ศูนย์ไม่ได้เป็นข้อมูล |
| ไม่ตรวจกรณีว่าง | จำนวนสมาชิกศูนย์ | ต้องไม่อ่านสมาชิกแรก |
กรณีทดสอบต้องมีผลที่คาดจากโจทย์หรือการคำนวณอิสระ หากคัดสูตรจากโปรแกรมไปคำนวณ expected ด้วยสูตรเดียวกัน ความผิดอาจถูกคัดไปทั้งสองที่ เช่น ใช้การหารจำนวนเต็มเหมือนกันทั้งผลจริงและผลคาด แล้วสรุปว่าผ่าน
เมื่อแก้บั๊ก ให้รันกรณีที่เคยผิดพร้อมกรณีสำคัญเดิม เพราะการแก้ขอบเขตหนึ่งอาจทำอีกขอบเขตผิดได้ ไม่จำเป็นต้องรันทุกเครื่องมือในระบบซ้ำโดยไม่มีเหตุผล แต่ควรรักษาหลักฐานว่าพฤติกรรมที่ได้รับผลยังถูก
ท้ายงานให้เลือกข้อทดสอบหนึ่งกรณีแล้วอธิบายว่าหากลบทิ้ง ความผิดแบบใดอาจหลุด หากตอบไม่ได้ให้พิจารณาว่ากรณีนี้ซ้ำกับกรณีอื่นหรือยังไม่ตรงความเสี่ยง การทดสอบที่ดีสื่อความต้องการและช่วยตัดสินใจแก้ไข ไม่ใช่เพียงเพิ่มจำนวนข้อความว่า passed
ตัวอย่าง regression: ทดสอบทั้งฟังก์ชันค้นหาและผู้ใช้ผลค้นหา
สร้างไฟล์ search_regression.c ค่าข้อมูลเป็นบวกจึงใช้ -1 แทนไม่พบได้ การทดสอบผู้เรียกช่วยตรวจว่าดัชนีศูนย์ยังอ่านค่าได้ และไม่พบแล้วไม่อ่าน values[-1]
/* search_regression.c : guard a returned index before reading */
#include <stdio.h>
int linear_search(const int values[], int count, int target);
int lookup_value(const int values[], int count, int target);
int main(void)
{
const int values[3] = {10, 20, 30};
const int counts[4] = {3, 3, 3, 0};
const int targets[4] = {10, 30, 99, 10};
const int expected[4] = {10, 30, -1, -1};
int passed = 0;
for (int i = 0; i < 4; ++i) {
int actual = lookup_value(values, counts[i], targets[i]);
if (actual == expected[i]) {
++passed;
printf("PASS case%d\n", i + 1);
} else {
printf("FAIL case%d expected=%d actual=%d\n",
i + 1, expected[i], actual);
}
}
printf("passed=%d total=4\n", passed);
return passed == 4 ? 0 : 1;
}
int linear_search(const int values[], int count, int target)
{
for (int i = 0; i < count; ++i) {
if (values[i] == target) {
return i;
}
}
return -1;
}
int lookup_value(const int values[], int count, int target)
{
int index = linear_search(values, count, target);
if (index == -1) {
return -1;
}
return values[index];
}กรณีทั้งสี่ครอบคลุมพบตัวแรก พบตัวสุดท้าย ไม่พบ และชุดว่าง ถ้าชุดข้อมูลปกติยอมรับค่า -1 ด้วย ค่าคืนนี้จะกำกวม ต้องเปลี่ยนสัญญาให้แยกสถานะกับค่าข้อมูล ไม่ยกแบบฝึกนี้ไปใช้กับทุกโดเมนโดยอัตโนมัติ
11.9 อ่านโค้ดที่ผิดและทำความเข้าใจข้อความวินิจฉัย
ตัวอย่างต่อไปนี้เป็นโค้ดฝึกแยกจากงานที่ต้องส่ง แสดงข้อผิดพลาดที่สัมพันธ์กับเนื้อหาบท พร้อมข้อความวินิจฉัยจริง วิธีอ่าน และฉบับแก้ คำว่า diagnostic ใช้รวมข้อความ error, warning และ note จึงไม่ควรเรียกทุกบรรทัดว่า error หรือถือว่าการไม่มี error แปลว่าโปรแกรมถูกตามโจทย์
ที่มาของ log: ผู้เขียนทดสอบด้วย GCC 11.4.0 บน Ubuntu ผ่าน Linux environment เมื่อ 24 ตุลาคม 2569 ใช้ C11 และตัวเลือกดังนี้ ชื่อไฟล์ เลขบรรทัด คอลัมน์ ถ้อยคำ และคำเตือนที่เปิดใช้อาจต่างจากแพลตฟอร์มการเรียน นักศึกษาไม่จำเป็นต้องเปลี่ยนเครื่องมือหรือการติดตั้งตามชุดทดสอบของผู้เขียน
-std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0ข้อความรูปแบบ bad.c:5:17 อ่านว่าไฟล์ bad.c บรรทัด 5 คอลัมน์ 17 ส่วน ^ และ ~ ช่วยชี้บริเวณที่ตรวจพบ จุดนี้อาจเป็นปลายเหตุจากบรรทัดก่อนหน้า error ทำให้การสร้างโปรแกรมล้มในกรณีนั้น warning เตือนความเสี่ยงแม้คำสั่งสร้างอาจจบสำเร็จ และ note ให้บริบท เช่น declaration เดิม ต้องอ่านคู่กัน (GCC: Diagnostics)
ตัวอย่างที่ผิด 1: assert จับผลคำนวณที่ละเมิดข้อคาดหวัง
ประเภท: assertion ล้มขณะรัน
test กำหนดว่าค่าเฉลี่ย 5 กับ 2 ต้องได้ 2.5 แต่ฟังก์ชันหาร int ก่อนคืน double จึงได้ 2.0 โปรแกรมคอมไพล์ผ่าน แต่ assertion เป็นเท็จและหยุดขณะรัน ตัวอย่างใช้ 2.5 ที่แทนได้พอดีในระบบทดสอบ ไม่ใช่คำแนะนำให้เทียบ floating point ทุกกรณีด้วย ==
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <assert.h>
#include <stdio.h>
double average(int total, int count)
{
return total / count;
}
int main(void)
{
assert(average(5, 2) == 2.5);
puts("PASS: average(5, 2)");
return 0;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 bad.c -o badไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม
สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น
ผลจากการรันฉบับผิด
stdout ว่างในการทดลองนี้ หากโปรแกรมหยุดก่อน flush ข้อมูล อาจไม่เห็นข้อความที่คาดว่าจะพิมพ์ก่อนหน้า
stderr:
bad: bad.c:9: main: Assertion `average(5, 2) == 2.5' failed.ตัวเก็บผลบน Linux อ่านสถานะเป็น -6 หมายถึงจบด้วยสัญญาณ SIGABRT ตามรูปแบบของ Python subprocess ไม่ใช่เลขที่ภาษา C รับประกัน และไม่ใช่ error code ของ compiler
อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
Assertion ... failed บอกนิพจน์ที่ไม่จริง พร้อมไฟล์ บรรทัด และฟังก์ชันที่ตรวจพบ ไม่ได้แปลว่า assert เสียหรือควรลบทิ้ง ต้องย้อนจาก expected ไปตรวจค่าจริงและนิพจน์ใน average การหยุดนี้ไม่ใช่ compiler error
โค้ดที่แก้แล้ว
#include <assert.h>
#include <stdio.h>
double average(int total, int count)
{
return (double)total / count;
}
int main(void)
{
assert(average(5, 2) == 2.5);
puts("PASS: average(5, 2)");
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
แก้ conversion ก่อนหารแล้วรัน test เดิมซ้ำ ต้องเห็น PASS และจบปกติ ไม่แก้ test ให้คาด 2.0 เพื่อให้ผ่าน หากใช้ฟังก์ชันกับข้อมูลอื่นต้องกำหนด count มากกว่าศูนย์และตรวจ tolerance ตามความเหมาะสม
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
PASS: average(5, 2)ตัวอย่างที่ผิด 2: ซ่อนคำสั่งสำคัญไว้ใน assert
ประเภท: ข้อผิดพลาดเชิงตรรกะที่ compiler อาจไม่เตือน
ตัวอย่างนี้คอมไพล์ด้วย -DNDEBUG เพื่อแสดงกรณีที่ assert ถูกปิด นิพจน์ภายใน assert จะไม่ถูกประเมิน จึงไม่เพิ่ม count โปรแกรมอาจดูถูกต้องใน build ที่เปิด assert แต่เปลี่ยนพฤติกรรมเมื่อปิด
ตัวเลือกเพิ่มเติมของกรณีนี้: -DNDEBUG
โค้ดที่ผิด — ไม่ใช้แทนฉบับแก้
#include <assert.h>
#include <stdio.h>
int main(void)
{
int count = 0;
assert(++count == 1);
printf("count=%d\n", count);
return 0;
}คำสั่งสร้างและข้อความที่ได้จริง
gcc -std=c11 -O0 -Wall -Wextra -Wpedantic -Wformat=2 -fdiagnostics-color=never -fmessage-length=0 -DNDEBUG bad.c -o badไม่มีข้อความวินิจฉัยจาก compiler ในการทดลองนี้ ทั้ง stdout และ stderr ว่าง การไม่มีข้อความเป็นผลที่สังเกต ไม่ใช่การรับรองความถูกต้องของโปรแกรม
สถานะจบของคำสั่งสร้างที่ตัวเก็บผลอ่านได้: 0 ขั้นสร้างสำเร็จ จากนั้นจึงตรวจพฤติกรรมขณะรันหรือใช้ชุดทดสอบตามประเภทข้างต้น
ผลจากการรันฉบับผิด
stdout:
count=0stderr ว่าง ไม่มี runtime diagnostic จากระบบในการทดลองนี้ ต้องใช้การเทียบผลกับข้อกำหนดเป็นหลักฐาน
สถานะจบที่ตัวเก็บผลอ่านได้: 0
ผลตามข้อกำหนดที่ใช้เทียบ:
count=1อ่านข้อความว่าแจ้งอะไรและผิดอย่างไร
ไม่มี compiler error และผล count=0 เป็นหลักฐานว่าคำสั่งที่ซ่อนอยู่ไม่ได้ทำงาน -DNDEBUG เป็นตัวเลือกการทดลองของผู้เขียน ไม่ใช่ error message จากระบบ ถ้าไม่เปิดตัวเลือกนี้ตัวอย่างผิดอาจแสดง 1 และซ่อนข้อบกพร่อง
โค้ดที่แก้แล้ว
#include <assert.h>
#include <stdio.h>
int main(void)
{
int count = 0;
++count;
assert(count == 1);
printf("count=%d\n", count);
return 0;
}เหตุผลของการแก้และสิ่งที่ต้องตรวจซ้ำ
แยก ++count ออกจาก assert ให้คำสั่งหลักทำงานเสมอ แล้วใช้ assert ตรวจสถานะภายหลังเท่านั้น รันทั้ง build ที่เปิดและปิด assert เพื่อยืนยันว่าพฤติกรรมหลักไม่ขึ้นกับการมีเครื่องมือตรวจ
ผลตรวจฉบับแก้จริง: คอมไพล์ด้วยตัวเลือกเดิมและเพิ่ม -Werror เพื่อให้ชุดทดสอบผู้เขียนไม่ยอมรับ warning ไม่มีข้อความวินิจฉัยและคำสั่งสร้างจบด้วยสถานะ 0 จากนั้นรันได้ stdout ดังนี้ และ stderr ว่าง
count=1ตรวจฉบับแก้ทั้งแบบมี -DNDEBUG และไม่มีตัวเลือกนั้นแล้ว ได้ count=1 ทั้งสองแบบ การเพิ่มตัวนับจึงไม่พึ่งการเปิด assert
ลำดับการอ่าน log เมื่อลองโปรแกรมของบทนี้
- ระบุว่าเกิดในขั้น compile, link, run หรือ test และแยกข้อความจากระบบออกจากข้อความที่โปรแกรมพิมพ์เอง
- อ่าน diagnostic แรกกับบริบทบรรทัดก่อนหน้า แล้วใช้ note เพื่อเทียบ declaration หรือชนิดข้อมูล อย่าแก้ทุกข้อความตามลำดับโดยไม่หาสาเหตุร่วม
- แก้ทีละสาเหตุ สร้างโปรแกรมใหม่ และตรวจว่ากำลังรันผล build ใหม่ การเติมวงเล็บ ปิด warning หรือลบ test อาจทำให้ log เงียบโดยยังผิดโจทย์
- ใช้ข้อมูลเดิมที่ทำให้ผิดทดสอบซ้ำ แล้วเพิ่มกรณีขอบเขตที่สัมพันธ์กับบท การรันจบสถานะศูนย์เพียงครั้งเดียวไม่แทนการตรวจข้อกำหนดครบถ้วน
ความหมายตามภาษาอ้างจากร่างมาตรฐาน C11 โดยข้อความ diagnostic เฉพาะถ้อยคำเป็นของเครื่องมือที่ใช้ ไม่ได้กำหนดให้ compiler ทุกตัวพิมพ์เหมือนกัน (WG14 N1570, GCC: Warning Options)
ปฏิบัติการที่ 11 การดีบักและการทดสอบระดับหน่วย
ปฏิบัติการนี้แบ่งเป็นสองส่วน ส่วนแรกฝึกเขียนการทดสอบระดับหน่วยด้วยกรณีทดสอบที่เทียบผลคาดหวังกับผลจริง และฝึกสังเกตพฤติกรรม PASS/FAIL ส่วนที่สองฝึกใช้ assert ตรวจเงื่อนไขที่ต้องเป็นจริงเสมอ ทั้งสองส่วนทำแยกกันได้ แต่ควรทำตามลำดับเพื่อให้เห็นการตรวจสอบความถูกต้องตั้งแต่การเทียบผลไปจนถึงการตรวจเงื่อนไข
ไฟล์ซอร์สโค้ดในปฏิบัติการนี้ให้ตั้งชื่อเป็นภาษาอังกฤษแบบ snake_case และบันทึกไว้ในโฟลเดอร์ของผู้เรียนเอง คำสั่งคอมไพล์ในเอกสารนี้ทดสอบด้วย gcc 13.3.0
Lab A: การทดสอบระดับหน่วยด้วยกรณีทดสอบ
สถานการณ์ ต้องการทดสอบฟังก์ชัน absolute ที่หาค่าสัมบูรณ์ของจำนวนเต็ม ด้วยกรณีทดสอบหลายค่า แล้วรายงานผลว่าแต่ละกรณีผ่านหรือไม่ และสรุปจำนวนกรณีที่ผ่าน เพื่อฝึกการเขียน test harness ที่เทียบผลคาดหวังกับผลจริง
เป้าหมาย ฝึก LLO11.1 และ LLO11.2 คือจำแนกและสังเกตข้อผิดพลาดผ่านผล FAIL และเขียนการทดสอบระดับหน่วยที่เทียบ expected กับ actual
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch11_unittest.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch11_unittest.c : การทดสอบระดับหน่วยด้วยกรณีทดสอบและการเทียบผลคาดหวัง */
#include <stdio.h>
int absolute(int x) {
if (x < 0) {
return -x;
}
return x;
}
int tests_run = 0;
int tests_passed = 0;
void check(const char *name, int expected, int actual) {
tests_run++;
if (expected == actual) {
tests_passed++;
printf("PASS: %s\n", name);
} else {
printf("FAIL: %s (คาดหวัง %d แต่ได้ %d)\n", name, expected, actual);
}
}
int main(void) {
check("absolute(5)", 5, absolute(5));
check("absolute(-7)", 7, absolute(-7));
check("absolute(0)", 0, absolute(0));
printf("ผ่าน %d จาก %d กรณี\n", tests_passed, tests_run);
return tests_passed == tests_run ? 0 : 1;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch11_unittest.c -o ch11_unittest
./ch11_unittestผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
PASS: absolute(5)
PASS: absolute(-7)
PASS: absolute(0)
ผ่าน 3 จาก 3 กรณีฟังก์ชัน absolute ใช้เงื่อนไข if จากบทที่ 4 ตรวจว่าค่านำเข้าน้อยกว่าศูนย์หรือไม่ หากใช่จึงคืนค่าที่กลับเครื่องหมาย ฟังก์ชัน check เทียบผลคาดหวังกับผลจริงด้วย expected == actual แล้วรายงาน PASS หรือ FAIL พร้อมนับผลลงในตัวแปร tests_run และ tests_passed กรณีทดสอบสามกรณีครอบคลุมค่าบวก ค่าลบ และศูนย์ จึงผ่านครบทั้งสามและสรุปว่าผ่าน 3 จาก 3 กรณี
ตัดสินใจเอง A.1 ต่อยอดโปรแกรมโดยเพิ่มกรณีทดสอบค่าขอบของตนเองอย่างน้อยหนึ่งกรณี แล้วทดลองแก้ฟังก์ชัน absolute ให้ผิดโดยเจตนา เช่น เปลี่ยนเงื่อนไขเป็น x > 0 เพื่อสังเกตว่ากรณีใดเปลี่ยนเป็น FAIL และข้อความรายงานค่าที่คาดหวังกับค่าที่ได้อย่างไร จากนั้นแก้กลับให้ถูกต้อง
ผลลัพธ์ที่คาดหวัง A.1 เมื่อแก้ฟังก์ชันให้ผิด กรณีที่รับค่าลบจะรายงาน FAIL พร้อมแสดงค่าที่คาดหวังและค่าที่ได้ที่ต่างกัน และเมื่อแก้กลับให้ถูก ทุกกรณีจะกลับมา PASS (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ A กรณีทดสอบครอบคลุมค่าปกติ ค่าขอบ และค่าลบ เทียบ expected กับ actual ไม่สลับลำดับ รายงาน PASS/FAIL และนับผลถูกต้อง คอมไพล์ผ่านไม่มีการแจ้งเตือน และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| ข้อความรายงานค่าที่คาดหวังกับค่าที่ได้สลับกัน | ส่งอาร์กิวเมนต์ให้ check สลับลำดับ expected กับ actual | ส่งผลคาดหวังเป็นอาร์กิวเมนต์ก่อนผลจริงเสมอตามลำดับที่นิยามไว้ |
| ทดสอบผ่านแต่ฟังก์ชันยังมีข้อผิดพลาด | กรณีทดสอบไม่ครอบคลุมค่าลบหรือค่าขอบ | เพิ่มกรณีทดสอบค่าลบและค่าขอบ เช่น ศูนย์ ให้ครบ |
| จำนวนกรณีที่ผ่านไม่ตรงกับความจริง | ลืมเพิ่มค่า tests_run หรือ tests_passed ในฟังก์ชัน check | เพิ่ม tests_run ทุกครั้งที่เรียก และเพิ่ม tests_passed เฉพาะเมื่อผ่าน |
เปิดหลังผู้เรียนออกแบบด้วยตนเองแล้ว
ใบงานตรวจ Lab A: รายงาน FAIL และสถานะจบต้องสอดคล้อง
เพิ่มค่าทดสอบ -1 และ 1 แล้วทดลองฉบับผิดที่เปลี่ยนเงื่อนไขเป็น x > 0 ในสำเนา สังเกตทั้งผลของค่าบวกและค่าลบ ไม่แก้ต้นฉบับเดียวจนย้อนดูสาเหตุไม่ได้
| สิ่งที่ตรวจ | ฉบับถูก | ฉบับผิด | หลังแก้กลับ |
|---|---|---|---|
| ข้อทดสอบใด PASS/FAIL | |||
| tests_run / tests_passed | |||
| expected กับ actual ในข้อความ | |||
| สถานะจบโปรแกรม |
Lab A ฉบับนี้คืน 0 เมื่อผ่านครบและคืน 1 เมื่อมี FAIL เพื่อให้เครื่องมือที่เรียกโปรแกรมต่อทราบผลได้ ไม่ใช้เพียงข้อความบนหน้าจอ ส่วน absolute ใช้ค่าที่ค่าสัมบูรณ์แทนได้ใน int เท่านั้น ไม่คำนวณ -INT_MIN เพื่อเดาผลวนกลับ
Lab B: การใช้ assert ตรวจเงื่อนไข
สถานการณ์ ต้องการใช้ assert ตรวจข้อสมมติภายในว่าตัวหารไม่เป็นศูนย์ในฟังก์ชัน divide เมื่อเปิด assertions โดยตรวจเงื่อนไขว่าตัวหารไม่เป็นศูนย์ก่อนทำการหาร เพื่อฝึกการใช้ assert ตรวจเงื่อนไขที่ต้องเป็นจริงเสมอ
เป้าหมาย ฝึก LLO11.3 คือใช้ assert ตรวจเงื่อนไขก่อนทำงานและเข้าใจพฤติกรรมเมื่อเงื่อนไขเป็นเท็จ
ขั้นปฏิบัติ
- สร้างไฟล์ชื่อ
ch11_assert.cด้วยโปรแกรมแก้ไขข้อความหรือ IDE - พิมพ์ซอร์สโค้ดต่อไปนี้ โดยจัดย่อหน้า 4 ช่องว่างและใส่คอมเมนต์ส่วนหัว
/* ch11_assert.c : การใช้ assert ตรวจเงื่อนไขที่ต้องเป็นจริงเสมอ */
#include <stdio.h>
#include <assert.h>
int divide(int a, int b) {
assert(b != 0);
return a / b;
}
int main(void) {
printf("10 / 2 = %d\n", divide(10, 2));
printf("20 / 4 = %d\n", divide(20, 4));
return 0;
}- คอมไพล์และรันด้วยคำสั่ง
gcc -std=c11 -Wall ch11_assert.c -o ch11_assert
./ch11_assertผลการรันจริง (ทดสอบแล้วด้วย gcc 13.3.0)
10 / 2 = 5
20 / 4 = 5เมื่อไม่ได้กำหนด NDEBUG ฟังก์ชัน divide ตรวจข้อสมมติ assert(b != 0) ก่อนหาร โดยรวมส่วนหัว #include <assert.h> ไว้ก่อน ทั้งสองการเรียกคือ divide(10, 2) และ divide(20, 4) มีตัวหารไม่เป็นศูนย์ เงื่อนไขจึงเป็นจริงและโปรแกรมทำงานต่อจนได้ผล 5 ทั้งสองกรณี หากมีการเรียก divide ด้วยตัวหารเป็นศูนย์ เช่น divide(10, 0) เงื่อนไข b != 0 จะเป็นเท็จ โปรแกรมจะหยุดทันทีพร้อมข้อความว่าการตรวจสอบล้มเหลว (assertion failed) และระบุชื่อไฟล์กับหมายเลขบรรทัดของ assert นั้น พฤติกรรมเมื่อเงื่อนไขเป็นเท็จนี้อธิบายไว้เพื่อความเข้าใจ โดยยังไม่ได้รันกรณีตัวหารเป็นศูนย์ในผลด้านบน
ตัดสินใจเอง B.1 ต่อยอดโปรแกรมโดยเขียนฟังก์ชันของตนเองที่มีเงื่อนไขซึ่งต้องเป็นจริงเสมอ เช่น ฟังก์ชันที่รับดัชนีอาร์เรย์ซึ่งต้องไม่ติดลบ แล้วเพิ่ม assert ตรวจเงื่อนไขนั้นก่อนทำงาน พร้อมเรียกใช้ด้วยค่าที่ถูกต้องเพื่อยืนยันว่าโปรแกรมทำงานต่อได้ตามปกติ
ผลลัพธ์ที่คาดหวัง B.1 เมื่อเรียกฟังก์ชันด้วยค่าที่ทำให้เงื่อนไขเป็นจริง โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือนและทำงานต่อจนจบตามปกติ (ผลที่คาดว่าจะเห็นสำหรับโปรแกรมที่ผู้เรียนออกแบบเอง ยังไม่ได้ทดสอบในเอกสารนี้ เพราะขึ้นกับการออกแบบของแต่ละคน)
จุดตรวจ B วาง assert ตรวจเงื่อนไขถูกจุดก่อนการทำงานที่ต้องการป้องกัน รวม #include <assert.h> ครบ โปรแกรมคอมไพล์ผ่านไม่มีการแจ้งเตือนและรันได้ผลถูก และโค้ดมีการจัดย่อหน้าและคอมเมนต์ส่วนหัว
ปัญหาที่พบบ่อย
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
คอมไพล์ไม่ผ่านที่บรรทัดที่ใช้ assert | ลืมรวมส่วนหัว #include <assert.h> | เพิ่ม #include <assert.h> ไว้ส่วนบนของไฟล์ |
assert ไม่ป้องกันข้อผิดพลาด | วาง assert ไว้หลังการทำงานที่ต้องการป้องกัน | วาง assert ไว้ก่อนการทำงานนั้นเสมอ เช่น ก่อนการหาร |
เข้าใจว่า assert ใช้จัดการข้อมูลนำเข้าที่ผิดปกติทั่วไป | สับสนระหว่างเงื่อนไขตรรกะที่ต้องจริงเสมอกับการตรวจข้อมูลนำเข้า | ใช้ assert กับเงื่อนไขที่หากผิดแสดงว่าตรรกะผิด ส่วนข้อมูลนำเข้าที่ผิดปกติจัดการด้วยเงื่อนไข if ปกติ |
เปิดหลังผู้เรียนส่งงานแล้ว
ใบงานตรวจ Lab B: ตรวจข้อสมมติและขอบเขตข้อมูลคนละหน้าที่
| สถานการณ์ | ควรใช้เครื่องมือใด | ผลที่ต้องสังเกต |
|---|---|---|
| ผู้ใช้กรอกตัวหาร 0 | if จัดการข้อมูล | ข้อความที่เข้าใจและสถานะผิดพลาด |
| ภายในเรียก divide ผิดสัญญาเมื่อ assertions เปิด | assert ตรวจข้อสมมติ | ตำแหน่งที่ล้มเหลว |
| ปิด assertions ด้วย NDEBUG | ตรวจการทำงานจำเป็นแยก | การอ่านและจัดการข้อมูลยังอยู่ |
| ทดสอบดัชนีอาร์เรย์ | สัญญาพื้นที่และข้อสมมติ | ต้องมีทั้งขอบล่างและขอบบน |
งาน B.1 ที่ตรวจดัชนีไม่ติดลบอย่างเดียวยังไม่ยืนยันว่าดัชนีน้อยกว่าจำนวนสมาชิก ฟังก์ชันต้องมี count ที่ถูกต้องด้วย และไม่ใช้ assert เป็นตัวรับผิดชอบข้อมูลผู้ใช้ที่ผิดตามปกติ
การเห็น assert ล้มเหลวบอกว่าตรวจพบการละเมิดข้อสมมติ ไม่ได้บอกสาเหตุทั้งหมดโดยอัตโนมัติ กลับไปดูผู้เรียกและข้อเท็จจริงก่อนเปลี่ยนหรือลบเงื่อนไข
เก็บกวาด
เมื่อจบปฏิบัติการ ให้ลบไฟล์ปฏิบัติการที่สร้างจากการคอมไพล์ (เช่น ch11_unittest และ ch11_assert) และเก็บเฉพาะไฟล์ซอร์สโค้ด .c ไว้ เพื่อให้โฟลเดอร์งานสะอาดและส่งไฟล์ซอร์สโค้ด .c พร้อมบันทึกผลการรันตามที่กำหนด
ตรวจความเข้าใจก่อนเริ่มบทที่ 12
ตอบโดยใช้บันทึกทำซ้ำ ข้อทดสอบ หรือสถานะโปรแกรมที่ทดลองแล้ว
- ทำไมคอมไพล์ผ่านและ exit 0 ยังไม่พิสูจน์ว่าผลตรงข้อกำหนด
- count เท่ากับ 2 และ sum เท่ากับ 7 ช่วยตัดสมมติฐานใดในปัญหาค่าเฉลี่ย
- ข้อมูลใดจับความต่างระหว่างมากกว่า 50 กับอย่างน้อย 50
- เหตุใดการอ่านข้อมูลหรือเพิ่มตัวนับจึงไม่ควรอยู่ใน assert
- เมื่อมี FAIL สถานะจบของ test harness ควรบอกอะไร
- การทดสอบค้นหาสำเร็จอย่างเดียวพลาดเส้นทางใดบ้าง
- หลังแก้บั๊กควรรันกรณีใดซ้ำ และหลักฐานที่เก็บควรมีอะไร
เมื่ออธิบายได้ ให้เตรียมใช้ชุดทดสอบนี้คุมผลลัพธ์ก่อนเปรียบเทียบประสิทธิภาพใน บทที่ 12 การวัดประสิทธิภาพและคุณภาพโค้ด โปรแกรมที่เร็วแต่ผลผิดยังไม่ผ่านข้อกำหนด
สรุป
บทนี้นำฟังก์ชันจากบทที่ 6 และเงื่อนไข if จากบทที่ 4 มาใช้ตรวจสอบความถูกต้องของโปรแกรมอย่างเป็นระบบ เริ่มจากการจำแนกชนิดข้อผิดพลาดสามชนิด คือ syntax error ที่คอมไพล์ไม่ผ่าน runtime error ที่เกิดขณะรัน และ logic error ที่คอมไพล์และรันได้แต่ผลผิด พร้อมการอ่านข้อความและหมายเลขบรรทัดจากคอมไพเลอร์และบทบาทของ -Wall ต่อด้วยเทคนิคการดีบักด้วยการแทรก printf ไล่ค่าตัวแปรเพื่อจำกัดขอบเขตของข้อผิดพลาด จากนั้นคือการทดสอบระดับหน่วยด้วยฟังก์ชัน check ที่เทียบผลคาดหวังกับผลจริงด้วย expected == actual รายงาน PASS/FAIL และนับผล โดยออกแบบกรณีทดสอบให้ครอบคลุมค่าปกติ ค่าขอบ และค่าลบ สุดท้ายคือการใช้ assert ตรวจเงื่อนไขที่ต้องเป็นจริงเสมอ เช่น assert(b != 0) ที่หยุดโปรแกรมพร้อมแจ้งตำแหน่งเมื่อเงื่อนไขเป็นเท็จ ทักษะเหล่านี้เป็นพื้นฐานของการพัฒนาโปรแกรมที่มีคุณภาพในบทถัดไป
สิ่งที่ส่งต่อไปยังบทถัดไป
| สิ่งที่ส่งต่อ | ค่า/รูปแบบ | ใช้ในบท |
|---|---|---|
| การจำแนกชนิดข้อผิดพลาด | syntax / runtime / logic error | บท 12 (คุณภาพโค้ด), บท 15 (โครงงาน) |
| เทคนิคการดีบักด้วย printf ไล่ค่า | แทรก printf พิมพ์ค่าตัวแปรในแต่ละขั้น | ทุกบทที่ต้องตรวจหาข้อผิดพลาด |
| การทดสอบระดับหน่วย | ฟังก์ชัน check เทียบ expected == actual รายงาน PASS/FAIL | บท 12 (คุณภาพโค้ด), บท 15 (โครงงาน) |
| การใช้ assert | #include <assert.h>, assert(เงื่อนไข) | บท 12 (คุณภาพโค้ด), บท 15 (โครงงาน) |
สรุปคำสั่ง
| คำสั่ง | หน้าที่ |
|---|---|
gcc -std=c11 -Wall ch11_unittest.c -o ch11_unittest | คอมไพล์ซอร์สโค้ด ch11_unittest.c เป็นไฟล์ปฏิบัติการ พร้อมเปิดการแจ้งเตือน |
gcc -std=c11 -Wall ch11_assert.c -o ch11_assert | คอมไพล์ซอร์สโค้ด ch11_assert.c เป็นไฟล์ปฏิบัติการ พร้อมเปิดการแจ้งเตือน |
./ch11_unittest หรือ ./ch11_assert | รันไฟล์ปฏิบัติการที่คอมไพล์แล้ว |
แบบฝึกหัดท้ายบท
ตอนที่ 1 ฝึกทบทวน (15 ข้อ ตรวจคำตอบให้ทันที)
ตอนที่ 2 แบบฝึกหัดเขียนตอบ (5 ข้อ)
กลุ่ม ก (เข้าใจ)
- ก1 (LLO11.1) จงอธิบายความต่างระหว่างข้อผิดพลาดสามชนิด คือ syntax error, runtime error และ logic error พร้อมยกตัวอย่างของแต่ละชนิด
- ก2 (LLO11.3) จงอธิบายว่า
assertต่างจากการตรวจเงื่อนไขด้วยifและprintfอย่างไร และเหมาะกับเงื่อนไขลักษณะใด
กลุ่ม ข (ประยุกต์)
- ข1 (LLO11.2) จงเขียนโปรแกรมภาษา C ที่มีฟังก์ชันหาค่ามากกว่าระหว่างจำนวนเต็มสองค่า แล้วเขียนฟังก์ชัน
checkทดสอบฟังก์ชันนั้นด้วยกรณีทดสอบอย่างน้อยสามกรณีที่ครอบคลุมค่าปกติ ค่าเท่ากัน และค่าลบ รายงาน PASS/FAIL แล้วคอมไพล์และรันให้ได้ผลจริง - ข2 (LLO11.3) จงเขียนฟังก์ชันที่รับจำนวนเต็มที่ต้องไม่เป็นศูนย์ แล้วใช้
assertตรวจเงื่อนไขก่อนทำงาน พร้อมรวมส่วนหัวที่จำเป็นและเรียกใช้ด้วยค่าที่ถูกต้อง
กลุ่ม ค (วิเคราะห์)
- ค1 (LLO11.1) จากโปรแกรมที่คอมไพล์ผ่านแต่ให้ผลลัพธ์ผิด จงวิเคราะห์ว่าเป็นข้อผิดพลาดชนิดใด และอธิบายขั้นตอนการใช้
printfไล่ค่าเพื่อหาจุดที่ค่าคลาดเคลื่อน
การศึกษาค้นคว้าเพิ่มเติม
- ศึกษาแนวคิดการตรวจสอบและการแก้จุดบกพร่องของโปรแกรมเพิ่มเติมจาก Kernighan และ Ritchie (1988) แล้วทดลองออกแบบกรณีทดสอบให้ครอบคลุมค่าขอบของฟังก์ชันที่เขียนเอง
- ทบทวนข้อกำหนดของแมโคร
assertและส่วนหัวassert.hในมาตรฐานภาษา C ตาม ISO/IEC 9899 เพื่อเข้าใจพฤติกรรมเมื่อเงื่อนไขเป็นเท็จและการปิดการทำงานของassertในขั้นการแจกจ่าย - ทดลองเขียน test harness ที่รวมหลายฟังก์ชันไว้ในชุดทดสอบเดียว แล้วสังเกตว่ากรณีทดสอบที่ครอบคลุมช่วยจับ logic error ที่คอมไพเลอร์ไม่แจ้งเตือนได้อย่างไร
เอกสารอ้างอิง
International Organization for Standardization. (2011). ISO/IEC 9899:2011 — Information technology — Programming languages — C. ISO.
Kernighan, B. W., & Ritchie, D. M. (1988). The C programming language (2nd ed.). Prentice Hall.
ตำราหลักภาษาไทยของรายวิชา. (ผู้สอนเติมรายละเอียดบรรณานุกรม — เพิ่มตามมติผู้สอน 21 ต.ค. 2569).
แหล่งปฐมภูมิที่ตรวจประกอบการปรับปรุง 23 ตุลาคม 2569
ใช้เอกสารข้างต้นตรวจหลักการก่อนเรียบเรียงส่วนขยายและสร้างข้อมูลสมมติ ตัวอย่างใหม่ที่เป็นโปรแกรม C ครบไฟล์ตรวจด้วย GCC 12.2.0 (Debian Linux) และ GCC 6.3.0 (MinGW-w64), C11 และคำเตือน -Wall -Wextra -Wpedantic เมื่อ 23 ตุลาคม 2569 แยกจากผลทดสอบ GCC 13.3.0 ของตัวอย่างเดิม ค่ารูปแบบและผลคำนวณตรวจจากการรัน ส่วนเวลา CPU ขึ้นกับสภาพแวดล้อม ไม่ใช่ผลตายตัว
แหล่งปฐมภูมิเพิ่มสำหรับชุดสอนพร้อมใช้บทที่ 11
- GNU C Library: Consistency Checking — ข้อสมมติภายใน NDEBUG และผลข้างเคียงใน assert
- GCC: Warning Options — ขอบเขตของคำเตือนและข้อจำกัดของ -Wall
- GCC: Instrumentation Options — เครื่องมือตรวจบางปัญหาระหว่างรัน ไม่ใช่หลักฐานว่าไม่มีบั๊กทุกชนิด
ตรวจเอกสารก่อนเขียนส่วนเพิ่มเมื่อ 23 ตุลาคม 2569 ตัวอย่างใช้ hosted C11 และข้อมูลสมมติ ผล PASS/FAIL ของฉบับผิดกับฉบับแก้เป็นคนละสิ่งกับผลการตรวจชุดสอน เวลาที่จัดในแผนไม่ใช่ผลวัดจากการสอนจริง