How Zephyr Testing Works
You're sold on testing — if not, back up a page. So how does Zephyr actually turn "I want to prove add(2, 3) is 5" into a green checkmark on your screen?
It comes down to two tools and one almost embarrassingly simple contract between them. Get this picture in your head and the next page is just typing.
Two tools, not one
The most common mix-up in Zephyr testing: assuming "ztest" is the whole story. It isn't. There are two tools, and they do completely different jobs.
ztest — the framework you write tests in. C macros,
#include <zephyr/ztest.h>. Twister — the tool that builds, runs, and reads the results.west twister, on your PC.
ztest is the what you write. Twister is the what runs it. One lives inside your source code; the other is a command you type.
| ztest | Twister | |
|---|---|---|
| Lives in | your C source | your terminal (west twister) |
| Its job | write tests + assertions | build, run, collect pass/fail |
| Form | C macros | command-line tool |
How they talk to each other
Here's the whole machine — two tools, four steps:
Now the part that surprises everyone: Twister doesn't understand C. It never parses your code. The only thing connecting your test to the report is plain text on a console.
ztest prints a fixed text format. Twister greps that text. That's the entire contract. 🪄
When a test runs, ztest prints something like:
START - test_add
PASS - test_add in 0.000 seconds
START - test_buffer
Assertion failed at test_calc.c:18: (6 not equal to add(2, 3))
FAIL - test_buffer in 0.000 seconds
START - <name>, then PASS or FAIL. You never write those lines — the macros emit them. Which means every Zephyr test on Earth prints identically, and that's the trick that lets one tool run thousands of tests it knows nothing about. The bonus: a failure prints file, line, and reason, so you land on the broken statement instantly instead of bisecting by hand.
Three ways to assert
You'll write assertions constantly. There are three families, and the only thing that differs is what happens when one fails:
| Macro | On failure |
|---|---|
zassert_* | ❌ stop the test right now |
zexpect_* | 📝 note it, keep going — the test still fails at the end |
zassume_* | ⏭️ skip the test |
assert = stop · expect = collect · assume = skip
Reach for zassert_* 95% of the time: check a thing, and the instant it's wrong, stop the test right there.
Integration or unit?
One last fork before we build. ztest runs your test in one of two styles:
- Integration — your code runs inside a real Zephyr image (a board, or
native_sim). Best for how modules actually behave together: timers, queues, drivers. - Unit — just the one module, compiled on its own, everything around it stubbed out (Zephyr ships FFF for the mocks). Faster and surgical, but more wiring. Linux-only.
👉 Start with integration on native_sim. Reach for unit tests the day a module gets gnarly enough that you want it alone on an island.
Recap
- ztest writes the checks (C). Twister runs them (
west twister). - They're joined by nothing but a fixed
START/PASS/FAILtext format. - Every failure hands you file + line + reason, free.
- Default to integration tests on
native_sim.
That's the entire mental model. Let's make it real → Write & run your first test: four files, three macros, and a test you'll watch pass and fail. ✅
Source: Zephyr Test Framework docs