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

Unit test บนเครื่องโฮสต์

เมื่อจบบทเรียนนี้ คุณจะ

  1. แยกฟังก์ชันตรรกะ เช่น ฟิลเตอร์หรือ state machine ออกจากโค้ดที่แตะฮาร์ดแวร์ เพื่อให้คอมไพล์บนเครื่องโฮสต์ได้
  2. เขียน unit test ด้วย Unity ครอบคลุมกรณีปกติ กรณีขอบ และกรณีผิดพลาด
  3. พิสูจน์ว่า test ล้มเหลวได้จริงโดยจงใจใส่บั๊กหนึ่งจุด ก่อนเชื่อผลที่ผ่าน

ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5) ทั้งบทรันบนคอมพิวเตอร์ ต้องมี gcc หรือ clang, make, git และ python3

ทวนจากโมดูลก่อนหน้าสองข้อ

  1. ตลอดหลักสูตรนี้ แบบฝึกทุกไฟล์ขึ้นสีแดงก่อนคุณเติม และมี test บางข้อที่ผ่านตั้งแต่ยังไม่เติม (บทเรียน 2.2, 3.2, 4.1 และ 5.3) test แบบนั้นพิสูจน์อะไรได้บ้าง
  2. ตัวถอดรหัส UART และ SPI ในโมดูล 5 รันบนคอมพิวเตอร์ได้ทั้งที่เป็นเรื่องของฮาร์ดแวร์ เพราะอะไร

ดึง Unity รุ่น v2.7.0 มาไว้ในโฟลเดอร์ examples ของบทนี้ (ไฟล์ .gitignore ของโฟลเดอร์กันไม่ให้มันถูก commit) แล้วรัน test สองข้อแรก

Terminal window
cd examples
git clone --depth 1 --branch v2.7.0 https://github.com/ThrowTheSwitch/Unity.git unity
make test

ทายก่อนรัน ว่าจะมีกี่ test และผลเป็นอย่างไร ผลคือ 2 Tests 0 Failures 0 Ignored และ OK ทั้งที่ examples/level_alarm.c เป็น state machine ที่ตั้งใจจะใช้บนบอร์ด ไม่มีบอร์ดต่ออยู่เลย จากนั้นรันอีกคำสั่งหนึ่ง

Terminal window
bash prove_red.sh test_level_alarm_first.c unity/src

สคริปต์นี้ใส่บั๊กลงใน level_alarm.c ทีละจุดในโฟลเดอร์ชั่วคราว แล้วรัน test ชุดเดิม ผลคือบั๊กสามในสี่ตัว SURVIVED test ที่ผ่านทั้งสองข้อตรวจโค้ดได้น้อยกว่าที่ OK ทำให้รู้สึกมาก บทเรียนนี้คือการทำให้ทั้งสี่ตัวถูกจับได้

โค้ดเฟิร์มแวร์มีสองส่วนที่ควรแยกกัน ส่วนที่ ตัดสินใจ (state machine ฟิลเตอร์ การถอดรหัสโปรโตคอล การคำนวณ) กับส่วนที่ แตะฮาร์ดแวร์ (เรียก PDL อ่านรีจิสเตอร์ รอ interrupt) ถ้าส่วนตัดสินใจเรียก Cy_GPIO_Read() ตรง ๆ มันคอมไพล์บนคอมพิวเตอร์ไม่ได้ และทดสอบได้ก็ต่อเมื่อมีบอร์ด ตะเข็บ (seam) คือจุดที่เราตัดสองส่วนออกจากกัน

SDK มี host test ที่ใช้ตะเข็บแบบนี้จริง test_arduino_shield.c ทดสอบตาราง capability ของ header บน QWA309 ตัวจริง โดยใส่ “Stub physical layer” แทนฟังก์ชันที่เรียกฮาร์ดแวร์ผ่าน struct arduino_ops_t และ Makefile ของ test อธิบายเหตุผลว่าตารางนั้น “is the piece most likely to be wrong and most expensive to debug on a bench, and it is portable C precisely so it can be checked here” ตัวถอดรหัสจอยสติ๊ก f310_parse() ก็มี host test ของตัวเองที่ build ด้วย gcc คำสั่งเดียว (test_hid_f310_parser.c)

ตัวอย่างของบทนี้ใช้รูปแบบเดียวกัน level_alarm.h อ่านค่าผ่าน level_sensor_t ที่มี function pointer read_mv บนคอมพิวเตอร์ test ใส่ตัวปลอมที่คืนค่าจากตาราง บนบอร์ดเราใส่ตัวจริง เช่นร่างข้างล่างที่อ่านโพเทนชิโอมิเตอร์ด้วย potentiometer_read_voltage() จาก sensor_potentiometer.h ของ SDK (ร่างนี้แสดงรูปแบบ หลักสูตรยังไม่ได้ทดสอบบนบอร์ด ต้องเรียก potentiometer_init() ก่อน และหัวไฟล์ระบุว่าไดรเวอร์ “Only compiled when BSP_HAS_POTENTIOMETER=1”)

static int pot_read_mv(void *ctx, int32_t *out_mv)
{
(void)ctx;
float v = 0.0f;
if (!potentiometer_read_voltage(&v)) { /* sensor_potentiometer.h ของ SDK: bool potentiometer_read_voltage(float *voltage) */
return -1;
}
*out_mv = (int32_t)(v * 1000.0f);
return 0;
}
static const level_sensor_t k_pot = {pot_read_mv, NULL};

Unity เป็น framework สำหรับ unit test ภาษา C ขนาดเล็ก สามไฟล์ใน src/ ใช้ได้ทั้งบนคอมพิวเตอร์และบนไมโครคอนโทรลเลอร์ โครงของไฟล์ test หนึ่งไฟล์

ส่วน ทำอะไร
setUp() / tearDown() เรียกก่อนและหลังทุก test ใช้ตั้งสถานะให้สะอาด หัวไฟล์ unity.h ระบุว่าถ้าใช้ Unity ตรง ๆ “these will need to be provided for each test executable”
static void test_...(void) test หนึ่งข้อ ใช้ assertion เช่น TEST_ASSERT_EQUAL_INT(expected, actual) TEST_ASSERT_TRUE(cond) TEST_ASSERT_EQUAL_HEX8(e, a)
main() UNITY_BEGIN(); ตามด้วย RUN_TEST(test_...) ทีละข้อ แล้ว return UNITY_END(); ซึ่งคืนจำนวน test ที่ล้ม ใช้เป็น exit code ของโปรแกรม

test ที่ดีครอบคลุมสามชนิด

  • กรณีปกติ สิ่งที่เกิดบ่อยที่สุด เช่นค่าต่ำกว่าเกณฑ์ ต้องไม่แจ้งเตือน
  • กรณีขอบ ค่าที่อยู่ตรงเส้น เช่นเท่ากับเกณฑ์พอดี จำนวนครั้งที่ขาดไปหนึ่ง ช่วง hysteresis ตัวนับที่วนกลับ บั๊กส่วนใหญ่อยู่ตรงนี้
  • กรณีผิดพลาด เซนเซอร์อ่านไม่ได้ อินพุตเป็น NULL ที่เก็บเต็ม ระบบต้องบอกความจริง (บทเรียน 3.2) และ test ต้องยืนยันว่ามันบอก

test ที่ผ่านบอกได้แค่ว่า “ไม่เจอความผิดพลาดที่ test นี้มองหา” ถ้า test มองหาไม่เป็น มันก็ผ่านเสมอ วิธีพิสูจน์คือ จงใจใส่บั๊ก (mutation) แล้วดูว่า test ล้มไหม ถ้าบั๊กรอด (survived) แปลว่ามีพฤติกรรมที่ไม่มี test ไหนเฝ้าอยู่ ขั้นตอนเล็ก ๆ ที่ทำได้ทุกครั้ง

  1. เขียน test ให้ล้มก่อน (red) เช่นเรียกฟังก์ชันที่ยังไม่มี หรือคาดหวังค่าที่โค้ดยังไม่คืน
  2. เขียนโค้ดให้ผ่าน (green) แล้วปรับให้สะอาด (refactor) นี่คือวงจรย่อของ TDD
  3. ก่อนเชื่อว่าเสร็จ ใส่บั๊กหนึ่งจุดที่ test นี้ควรจับ แล้วดูให้มันล้ม จากนั้นเอาบั๊กออก

SDK ใช้หลักเดียวกันกับเครื่องมือของตัวเอง แคตตาล็อกตัวอย่างอธิบายว่าตัวตรวจความครอบคลุมของ API วัดจาก symbol table ของ object file จริง ไม่ใช่การค้นข้อความ เพราะ “A mention in a comment produces no symbol and therefore no coverage” ตัวตรวจจึงล้มได้จริงเมื่อ API ใหม่ไม่มีตัวอย่าง (README ของแคตตาล็อก หัวข้อ 6)

โฟลเดอร์ examples/ มีห้าไฟล์ที่ทำงานร่วมกัน

  • level_alarm.h และ level_alarm.c state machine แจ้งเตือนระดับ มีสามสถานะ NORMAL, ACTIVE, FAULT ต้องเห็นค่าเกินเกณฑ์ติดกัน confirm ครั้งจึงเปลี่ยนสถานะ และมี hysteresis ระหว่าง on_mv กับ off_mv
  • test_level_alarm_first.c test สองข้อด้วย Unity เป็นสามท่า ท่าที่ 1 กรณีปกติ ท่าที่ 2 กรณีผิดพลาด ท่าที่ 3 main() ที่คืนจำนวน test ที่ล้ม
  • Makefile build และรันด้วย -Werror แบบเดียวกับ host test ของ SDK เลือกไฟล์ test ได้ด้วย TEST=
  • prove_red.sh ใส่บั๊กสี่แบบทีละตัว แล้วรายงานว่า test จับได้ (killed) หรือไม่ (SURVIVED)

ลองแก้แล้วทายก่อนรัน: เปลี่ยน >= เป็น > ใน level_alarm.c ด้วยมือ แล้วรัน make test test สองข้อแรกผ่านหรือล้ม เพราะอะไร (แล้วแก้กลับ)

เปิด practice/test_level_alarm.c มี test สองข้อแรกให้แล้ว และช่องให้เติม 4 ข้อ ตอนนี้แต่ละข้อเรียก TEST_FAIL_MESSAGE จึงขึ้นสีแดง

  1. ค่าเท่ากับเกณฑ์พอดีต้องนับ
  2. ค่าสูงต้องติดกัน ขาดหนึ่งครั้งเริ่มนับใหม่
  3. hysteresis ต้องค้าง ACTIVE ในช่วงระหว่างสองเกณฑ์
  4. อ่านล้มแล้วฟื้นต้องกลับเป็น NORMAL
Terminal window
cd examples
make test TEST=../practice/test_level_alarm.c
bash prove_red.sh ../practice/test_level_alarm.c unity/src

งานจะเสร็จเมื่อ make test ได้ 6 Tests 0 Failures และ prove_red.sh ขึ้น killed ครบทั้งสี่บรรทัด

ลองเองก่อนอย่างน้อย 15 นาที แล้วเปิด solution/test_level_alarm.c เราตรวจเฉลยแล้วว่า 6 Tests 0 Failures และ prove_red.sh จับบั๊กได้ครบทั้งสี่แบบ ขณะที่ test สองข้อแรกอย่างเดียวจับได้แบบเดียว สังเกตบรรทัดใน test ของ hysteresis ที่ตรวจว่าเป็น ACTIVE จริงก่อนเริ่มส่วนที่เหลือ ถ้าเงื่อนไขตั้งต้นไม่จริง ส่วนที่เหลือของ test จะผ่านหรือล้มโดยไม่ได้พิสูจน์อะไร

ตอบคำถาม 5 ข้อใน quiz.yaml (บนเว็บไซต์อยู่ท้ายหน้านี้) ครอบคลุมเป้าหมายทั้งสามข้อ ตอบถูกตั้งแต่ 4 ข้อขึ้นไปถือว่าจบบทเรียน

งาน: นำตรรกะหนึ่งชิ้นจากงานในหลักสูตรนี้ไปอยู่หลังตะเข็บ เขียน test ด้วย Unity และพิสูจน์ด้วย mutation

  1. เลือกหนึ่งอย่าง: ตัวกันเด้งจากบทเรียน 4.1, ผู้ดูแล watchdog จากบทเรียน 4.3 หรือตัวถอดรหัส UART จากบทเรียน 5.1 ย้ายฟังก์ชันตรรกะไปไว้ในไฟล์ .c กับ .h ของมันเอง โดยไม่มี #include ของ PDL หรือ FreeRTOS
  2. เขียน test ด้วย Unity อย่างน้อยห้าข้อ ปกติหนึ่ง ขอบอย่างน้อยสอง ผิดพลาดอย่างน้อยหนึ่ง
  3. เขียนบั๊กสามแบบที่คุณคิดว่าเป็นไปได้จริงสำหรับโค้ดนั้น (ใช้ prove_red.sh เป็นแบบ หรือแก้มือทีละตัว) แล้วบันทึกว่าแต่ละตัวถูกจับหรือรอด ถ้ารอด เพิ่ม test จนจับได้
  4. ถ้ามีบอร์ด เขียนตัวอ่านจริงสำหรับตะเข็บนั้น (เช่นใช้ Cy_GPIO_Read() กับตัวกันเด้ง) แล้ว build เข้ากับแม่แบบของ SDK ตรรกะไฟล์เดียวกันต้องใช้ได้ทั้งสองที่โดยไม่แก้

หลักฐานที่เก็บไว้ใน portfolio: ไฟล์ตรรกะ ไฟล์ test ผลของ make test ตารางบั๊กที่ใส่และผล (killed หรือ survived ก่อนและหลังเพิ่ม test) และถ้าทำข้อ 4 ให้แนบ log จากบอร์ด

  • อ่าน test ของ SDK ทั้งไฟล์ test_hid_f310_parser.c แล้วจัดกลุ่ม test ของมันเป็นปกติ ขอบ และผิดพลาด กลุ่มไหนมีน้อยที่สุด
  • เอกสาร Unity test framework มีตัวสร้าง test runner อัตโนมัติ และ Ceedling ที่รวม mock ให้ ลองเทียบกับการเขียน main() เอง

บทถัดไป: บทเรียน 6.2 CI สำหรับเฟิร์มแวร์ ให้เครื่องรัน test เหล่านี้ให้ทุกครั้งที่มีการเปลี่ยนแปลง

  • test ที่คุณเคยเขียน มีกี่ข้อที่คุณเคยเห็นมันล้มจริงสักครั้ง
  • ส่วนไหนของเฟิร์มแวร์ที่คุณคิดว่า “ทดสอบบนคอมพิวเตอร์ไม่ได้” ลองหาดูว่ามีตรรกะชิ้นไหนในนั้นที่แยกออกมาได้

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. ฟังก์ชันหนึ่งอ่านปุ่มด้วย Cy_GPIO_Read() แล้วตัดสินใจกันเด้งในฟังก์ชันเดียวกัน วิธีใดทำให้ทดสอบตรรกะกันเด้งบนคอมพิวเตอร์ได้โดยไม่แก้ตรรกะ (เป้าหมายข้อ 1)

    1. ให้ตรรกะรับค่าที่อ่านได้ผ่านพารามิเตอร์หรือ function pointer แล้วให้โค้ดบนบอร์ดเป็นผู้เรียก Cy_GPIO_Read()
    2. ใส่ #include ของ PDL ในไฟล์ test แล้วคอมไพล์ด้วย gcc บนคอมพิวเตอร์
    3. ทดสอบบนบอร์ดเท่านั้น เพราะโค้ดที่เกี่ยวกับปุ่มทดสอบบนคอมพิวเตอร์ไม่ได้
    4. ใช้ printf ในฟังก์ชันแล้วอ่านผลด้วยตา
    ดูเฉลย

    คำตอบ: A. ให้ตรรกะรับค่าที่อ่านได้ผ่านพารามิเตอร์หรือ function pointer แล้วให้โค้ดบนบอร์ดเป็นผู้เรียก Cy_GPIO_Read()

    แยกตะเข็บ ให้ตรรกะไม่รู้จักฮาร์ดแวร์ แบบเดียวกับ level_sensor_t ของบทนี้ และ arduino_ops_t ที่ host test ของ SDK ใส่ stub แทน ส่วน PDL ของบอร์ดคอมไพล์บนคอมพิวเตอร์ไม่ได้ Makefile ของ SDK จึงแทนครึ่งที่เป็น PDL ด้วย stub

  2. ไฟล์ .c ใดบ้างที่ควรคอมไพล์ได้ทั้งบนคอมพิวเตอร์และบนบอร์ดโดยไม่แก้ (เลือกได้หลายข้อ) (เป้าหมายข้อ 1)

    1. state machine แจ้งเตือนระดับที่อ่านค่าผ่าน function pointer
    2. ตัวถอดรหัสเฟรม UART ที่รับอาร์เรย์ของไบต์
    3. ตัวตั้งค่า SCB ที่เรียก Cy_SCB_UART_Init()
    4. ตัวจัดการ interrupt ที่อ่านรีจิสเตอร์ของ TCPWM
    ดูเฉลย

    คำตอบ: A. state machine แจ้งเตือนระดับที่อ่านค่าผ่าน function pointer · B. ตัวถอดรหัสเฟรม UART ที่รับอาร์เรย์ของไบต์

    สองข้อแรกเป็นตรรกะล้วน รับข้อมูลเข้าและคืนผล สองข้อหลังแตะฮาร์ดแวร์โดยตรง เป็นครึ่งที่อยู่บนบอร์ดและควรบางที่สุด

  3. state machine ของบทนี้เปลี่ยนเป็น ACTIVE เมื่อค่ามากกว่าหรือเท่ากับ on_mv ติดกัน confirm ครั้ง test ข้อใดเป็นกรณีขอบ (เป้าหมายข้อ 2)

    1. ค่าเท่ากับ on_mv พอดี confirm ครั้ง ต้องเป็น ACTIVE
    2. ค่าต่ำกว่า on_mv มาก ต้องเป็น NORMAL
    3. เซนเซอร์อ่านไม่ได้ ต้องเป็น FAULT
    4. เรียก level_alarm_init() แล้วตรวจว่าไม่ crash
    ดูเฉลย

    คำตอบ: A. ค่าเท่ากับ on_mv พอดี confirm ครั้ง ต้องเป็น ACTIVE

    กรณีขอบอยู่ตรงเส้น เท่ากับเกณฑ์พอดี ข้อสองเป็นกรณีปกติ ข้อสามเป็นกรณีผิดพลาด ถ้าเปลี่ยน >= เป็น > ใน level_alarm.c มีเพียง test แบบข้อแรกที่จับได้

  4. prove_red.sh รายงาน SURVIVED สำหรับบั๊ก 'no reset on a gap' หมายความว่าอะไร (เป้าหมายข้อ 3)

    1. ไม่มี test ใดล้มเมื่อโค้ดไม่ล้างตัวนับตอนค่าขาดช่วง จึงยังไม่มี test เฝ้าพฤติกรรม 'ต้องติดกัน'
    2. โค้ดจริงมีบั๊กนี้อยู่
    3. mutant คอมไพล์ไม่ผ่าน
    4. test ทั้งหมดผ่าน จึงเสร็จแล้ว
    ดูเฉลย

    คำตอบ: A. ไม่มี test ใดล้มเมื่อโค้ดไม่ล้างตัวนับตอนค่าขาดช่วง จึงยังไม่มี test เฝ้าพฤติกรรม 'ต้องติดกัน'

    SURVIVED คือ test ผ่านทั้งที่โค้ดผิด แก้ด้วยการเพิ่ม test ที่ใส่ค่าสูง ต่ำ สูง แล้วคาดหวังว่ายังไม่ ACTIVE ส่วน mutant ที่คอมไพล์ไม่ผ่านสคริปต์รายงานเป็น BROKEN และนับเป็นความล้มเหลว เพราะไม่ได้พิสูจน์อะไร

  5. ข้อใดเป็นหลักฐานที่ดีว่า test ชุดหนึ่งทดสอบอะไรจริง (เลือกได้หลายข้อ) (เป้าหมายข้อ 3)

    1. เคยเห็น test ล้มเมื่อจงใจใส่บั๊กที่มันควรจับ แล้วผ่านเมื่อเอาบั๊กออก
    2. test เขียนก่อนโค้ดและขึ้นสีแดงก่อนเขียนโค้ด
    3. ขึ้น OK ตั้งแต่รันครั้งแรก
    4. จำนวน test มากกว่าจำนวนฟังก์ชัน
    ดูเฉลย

    คำตอบ: A. เคยเห็น test ล้มเมื่อจงใจใส่บั๊กที่มันควรจับ แล้วผ่านเมื่อเอาบั๊กออก · B. test เขียนก่อนโค้ดและขึ้นสีแดงก่อนเขียนโค้ด

    สองข้อแรกคือการเห็นสีแดงจริง ข้อสามไม่พิสูจน์อะไร test สองข้อแรกของบทนี้ขึ้น OK แต่บั๊กสามในสี่แบบรอด ข้อสี่เป็นจำนวน ไม่ใช่ความสามารถในการจับบั๊ก

อ้างอิงบทเรียนนี้

ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน

"Unit test บนเครื่องโฮสต์" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ข้อความอ้างอิงภาษาอังกฤษ: "Unit tests on the host" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0

ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/embedded-c-foundations/m06-test-and-ci/l01-unit-tests-on-host/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).

วิธีอ้างอิง TESA ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA