Unit tests on the host
Objectives
Section titled “Objectives”By the end of this lesson, you will be able to
- Split a logic function — such as a filter or a state machine — from hardware-touching code, so it compiles on the host.
- Write Unity tests covering normal, edge, and error cases.
- Prove a test can genuinely fail, by planting one bug deliberately, before trusting a passing result.
Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5). The whole lesson runs on your computer; you need gcc or clang, make, git and python3.
Before you start
Section titled “Before you start”Two review questions from earlier modules.
- Throughout this course, every exercise file starts red before you fill it in, and some tests already pass before anything is filled in (lessons 2.2, 3.2, 4.1 and 5.3). What can that kind of test prove, and what can’t it?
- The UART and SPI decoders in module 5 run on a computer even though they’re about hardware — why is that possible?
See it work first
Section titled “See it work first”Fetch Unity v2.7.0 into this lesson’s examples folder (the folder’s .gitignore keeps it from being committed), then run the first two tests.
cd examplesgit clone --depth 1 --branch v2.7.0 https://github.com/ThrowTheSwitch/Unity.git unitymake testPredict before you run it: how many tests will there be, and what will the result say? The result is 2 Tests 0 Failures 0 Ignored and OK, even though examples/level_alarm.c is a state machine meant for the board, with no board connected at all. Now run one more command:
bash prove_red.sh test_level_alarm_first.c unity/srcThis script plants four bugs into level_alarm.c, one at a time, in a temporary folder, then runs the same tests. The result: three of the four bugs SURVIVED. The two tests that pass check far less than that OK makes it feel like. This lesson is about getting all four caught.
Concepts
Section titled “Concepts”1. The seam between logic and hardware
Section titled “1. The seam between logic and hardware”Firmware code has two parts that should stay separate: the part that decides (a state machine, a filter, protocol decoding, a calculation) and the part that touches hardware (calling the PDL, reading registers, waiting on an interrupt). If the decision-making part calls Cy_GPIO_Read() directly, it can’t compile on a computer, and it can only be tested with a board attached. A seam is the point where we cut these two parts apart.
The SDK has a real host test that uses exactly this kind of seam. test_arduino_shield.c tests the capability table for the header on a real QWA309, by supplying a “Stub physical layer” in place of the functions that call hardware, through the arduino_ops_t struct. The test’s Makefile explains why that table “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.” The joystick decoder f310_parse() has its own host test too, buildable with a single gcc command (test_hid_f310_parser.c).
This lesson’s example follows the same pattern. level_alarm.h reads a value through a level_sensor_t holding a read_mv function pointer. On a computer, the test supplies a fake that returns values from a table; on the board, we supply a real one — such as this draft, which reads a potentiometer with potentiometer_read_voltage() from the SDK’s sensor_potentiometer.h (this draft illustrates the pattern; this course hasn’t tested it on the board — potentiometer_init() must be called first, and the header notes the driver is “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)) { /* SDK's sensor_potentiometer.h: 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};2. Unity and three kinds of case
Section titled “2. Unity and three kinds of case”Unity is a small C unit test framework — three files in src/ that work on both a computer and a microcontroller. The layout of one test file:
| Part | What it does |
|---|---|
setUp() / tearDown() |
Called before and after every test, to reset state to a clean slate. The unity.h header notes that if you use Unity directly, “these will need to be provided for each test executable” |
static void test_...(void) |
One test, using assertions such as TEST_ASSERT_EQUAL_INT(expected, actual), TEST_ASSERT_TRUE(cond), TEST_ASSERT_EQUAL_HEX8(e, a) |
main() |
UNITY_BEGIN();, followed by RUN_TEST(test_...) for each test, then return UNITY_END();, which returns the number of failed tests as the program’s exit code |
A good set of tests covers three kinds of case.
- Normal cases: what happens most often, such as a value below the threshold, which must never alarm.
- Edge cases: values right on the line — exactly at the threshold, one missed reading short, the hysteresis band, a counter wrapping around. Most bugs live here.
- Error cases: the sensor fails to read, the input is NULL, storage is full. The system must tell the truth (lesson 3.2), and the test must confirm that it does.
3. A test that cannot fail is a test that tests nothing
Section titled “3. A test that cannot fail is a test that tests nothing”A passing test can only say “this test didn’t find the fault it was looking for.” If a test isn’t looking for something, it always passes. The way to prove otherwise is to deliberately plant a bug (a mutation) and see whether the test fails. If the bug survives, there’s behaviour no test is watching. A small routine you can do every time:
- Write a test that fails first (red) — for example, calling a function that doesn’t exist yet, or expecting a value the code doesn’t yet return.
- Write the code until it passes (green), then clean it up (refactor). This is the small TDD loop.
- Before trusting that it’s done, plant one bug this test is supposed to catch, confirm it fails, then remove the bug.
The SDK follows the same principle with its own tooling. The example catalogue explains that its API coverage checker measures coverage from a real object file’s symbol table, not by searching text, because “A mention in a comment produces no symbol and therefore no coverage” — so the checker genuinely fails when a new API has no example (the catalogue’s README, section 6).
Worked example
Section titled “Worked example”The examples/ folder has five files that work together.
- level_alarm.h and level_alarm.c: a level-alarm state machine with three states — NORMAL, ACTIVE, FAULT. It requires seeing values over the threshold for
confirmconsecutive readings before changing state, with hysteresis betweenon_mvandoff_mv. - test_level_alarm_first.c: two Unity tests, in three parts. Part 1: a normal case. Part 2: an error case. Part 3:
main(), returning the number of failed tests. - Makefile: builds and runs with
-Werror, the same way the SDK’s host tests do; the test file can be chosen withTEST=. - prove_red.sh: plants four kinds of bug one at a time, and reports whether the tests caught it (
killed) or not (SURVIVED).
Try changing things and predicting the result before you run it: change >= to > by hand in level_alarm.c, then run make test. Do the first two tests pass or fail, and why? (Then change it back.)
Practice
Section titled “Practice”Open practice/test_level_alarm.c. The first two tests are already there, and there are 4 gaps to fill in — each currently calls TEST_FAIL_MESSAGE, so they show red.
- A value exactly equal to the threshold must count.
- High values must be consecutive; one miss restarts the count.
- Hysteresis must keep it ACTIVE for values between the two thresholds.
- A failed read that recovers must return to NORMAL.
cd examplesmake test TEST=../practice/test_level_alarm.cbash prove_red.sh ../practice/test_level_alarm.c unity/srcThe work is done once make test reports 6 Tests 0 Failures and prove_red.sh shows killed on all four lines.
Solution
Section titled “Solution”Try it yourself for at least 15 minutes first, then open solution/test_level_alarm.c. We checked the solution: it gets 6 Tests 0 Failures, and prove_red.sh catches all four kinds of bug, while the first two tests alone caught only one. Notice the line in the hysteresis test that checks it’s genuinely ACTIVE before starting the rest of the test — if that starting condition isn’t true, the rest of the test passes or fails without proving anything.
Check your understanding
Section titled “Check your understanding”Answer the 5 questions in quiz.yaml (shown at the bottom of this page on the website), covering all three objectives. Getting 4 or more right counts as finishing the lesson.
Task: take one piece of logic from earlier work in this course, move it behind a seam, write Unity tests for it, and prove them with mutation.
- Choose one: the debouncer from lesson 4.1, the watchdog supervisor from lesson 4.3, or the UART decoder from lesson 5.1. Move the logic function into its own
.cand.hfiles, with no#includeof the PDL or FreeRTOS. - Write at least five Unity tests: at least one normal case, at least two edge cases, at least one error case.
- Write three kinds of bug you think are realistic for that code (use
prove_red.shas a model, or edit by hand one at a time), and record whether each was caught or survived. If one survives, add tests until it’s caught. - If you have a board, write a real reader for that seam (for example, using
Cy_GPIO_Read()for the debouncer), then build it into the SDK’s template. The same logic file must work in both places, unchanged.
Evidence to keep in your portfolio: the logic file, the test file, the make test output, a table of the bugs planted and their results (killed or survived, before and after adding tests), and, if you did step 4, the log from the board.
Going further
Section titled “Going further”- Read the SDK’s test_hid_f310_parser.c test file in full, then sort its tests into normal, edge, and error. Which category has the fewest?
- The Unity test framework documentation covers an automatic test-runner generator, and Ceedling, which bundles in mocking. Compare it against writing your own
main().
Next lesson: lesson 6.2, CI for firmware, which has a machine run these tests automatically on every change.
Reflect
Section titled “Reflect”- Of the tests you’ve written before, how many have you actually seen fail, even once?
- Which part of your firmware do you think of as “impossible to test on a computer”? Try finding a piece of logic inside it that can be pulled out.
References
Section titled “References”- ThrowTheSwitch Unity @ v2.7.0
- Unity test framework
- SDK: arduino_shield/test (a host test using a hardware stub)
- SDK: usb_hid_joystick/tests/test_hid_f310_parser.c
Review questions
Answer on your own first, then open the answer.
-
One function reads a button with Cy_GPIO_Read() and debounces in the same body. Which change lets the debounce logic be tested on a host without changing the logic? (Objective 1)
- ให้ตรรกะรับค่าที่อ่านได้ผ่านพารามิเตอร์หรือ function pointer แล้วให้โค้ดบนบอร์ดเป็นผู้เรียก Cy_GPIO_Read()
- ใส่ #include ของ PDL ในไฟล์ test แล้วคอมไพล์ด้วย gcc บนคอมพิวเตอร์
- ทดสอบบนบอร์ดเท่านั้น เพราะโค้ดที่เกี่ยวกับปุ่มทดสอบบนคอมพิวเตอร์ไม่ได้
- ใช้ printf ในฟังก์ชันแล้วอ่านผลด้วยตา
Show answer
Answer: A. ให้ตรรกะรับค่าที่อ่านได้ผ่านพารามิเตอร์หรือ function pointer แล้วให้โค้ดบนบอร์ดเป็นผู้เรียก Cy_GPIO_Read()
แยกตะเข็บ ให้ตรรกะไม่รู้จักฮาร์ดแวร์ แบบเดียวกับ level_sensor_t ของบทนี้ และ arduino_ops_t ที่ host test ของ SDK ใส่ stub แทน ส่วน PDL ของบอร์ดคอมไพล์บนคอมพิวเตอร์ไม่ได้ Makefile ของ SDK จึงแทนครึ่งที่เป็น PDL ด้วย stub
-
Which .c files should compile unchanged on both the host and the board? (choose all that apply) (Objective 1)
- state machine แจ้งเตือนระดับที่อ่านค่าผ่าน function pointer
- ตัวถอดรหัสเฟรม UART ที่รับอาร์เรย์ของไบต์
- ตัวตั้งค่า SCB ที่เรียก Cy_SCB_UART_Init()
- ตัวจัดการ interrupt ที่อ่านรีจิสเตอร์ของ TCPWM
Show answer
Answer: A. state machine แจ้งเตือนระดับที่อ่านค่าผ่าน function pointer · B. ตัวถอดรหัสเฟรม UART ที่รับอาร์เรย์ของไบต์
สองข้อแรกเป็นตรรกะล้วน รับข้อมูลเข้าและคืนผล สองข้อหลังแตะฮาร์ดแวร์โดยตรง เป็นครึ่งที่อยู่บนบอร์ดและควรบางที่สุด
-
The lesson's state machine goes ACTIVE after confirm consecutive readings at or above on_mv. Which test is an edge case? (Objective 2)
- ค่าเท่ากับ on_mv พอดี confirm ครั้ง ต้องเป็น ACTIVE
- ค่าต่ำกว่า on_mv มาก ต้องเป็น NORMAL
- เซนเซอร์อ่านไม่ได้ ต้องเป็น FAULT
- เรียก level_alarm_init() แล้วตรวจว่าไม่ crash
Show answer
Answer: A. ค่าเท่ากับ on_mv พอดี confirm ครั้ง ต้องเป็น ACTIVE
กรณีขอบอยู่ตรงเส้น เท่ากับเกณฑ์พอดี ข้อสองเป็นกรณีปกติ ข้อสามเป็นกรณีผิดพลาด ถ้าเปลี่ยน >= เป็น > ใน level_alarm.c มีเพียง test แบบข้อแรกที่จับได้
-
prove_red.sh reports SURVIVED for the 'no reset on a gap' bug. What does that mean? (Objective 3)
- ไม่มี test ใดล้มเมื่อโค้ดไม่ล้างตัวนับตอนค่าขาดช่วง จึงยังไม่มี test เฝ้าพฤติกรรม 'ต้องติดกัน'
- โค้ดจริงมีบั๊กนี้อยู่
- mutant คอมไพล์ไม่ผ่าน
- test ทั้งหมดผ่าน จึงเสร็จแล้ว
Show answer
Answer: A. ไม่มี test ใดล้มเมื่อโค้ดไม่ล้างตัวนับตอนค่าขาดช่วง จึงยังไม่มี test เฝ้าพฤติกรรม 'ต้องติดกัน'
SURVIVED คือ test ผ่านทั้งที่โค้ดผิด แก้ด้วยการเพิ่ม test ที่ใส่ค่าสูง ต่ำ สูง แล้วคาดหวังว่ายังไม่ ACTIVE ส่วน mutant ที่คอมไพล์ไม่ผ่านสคริปต์รายงานเป็น BROKEN และนับเป็นความล้มเหลว เพราะไม่ได้พิสูจน์อะไร
-
Which are good evidence that a test suite really tests something? (choose all that apply) (Objective 3)
- เคยเห็น test ล้มเมื่อจงใจใส่บั๊กที่มันควรจับ แล้วผ่านเมื่อเอาบั๊กออก
- test เขียนก่อนโค้ดและขึ้นสีแดงก่อนเขียนโค้ด
- ขึ้น OK ตั้งแต่รันครั้งแรก
- จำนวน test มากกว่าจำนวนฟังก์ชัน
Show answer
Answer: A. เคยเห็น test ล้มเมื่อจงใจใส่บั๊กที่มันควรจับ แล้วผ่านเมื่อเอาบั๊กออก · B. test เขียนก่อนโค้ดและขึ้นสีแดงก่อนเขียนโค้ด
สองข้อแรกคือการเห็นสีแดงจริง ข้อสามไม่พิสูจน์อะไร test สองข้อแรกของบทนี้ขึ้น OK แต่บั๊กสามในสี่แบบรอด ข้อสี่เป็นจำนวน ไม่ใช่ความสามารถในการจับบั๊ก
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"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
Thai attribution: "Unit test บนเครื่องโฮสต์" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
This lesson adapts the source below; keep its credit too.
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 Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA