Skip to main content

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.

ztestTwister
Lives inyour C sourceyour terminal (west twister)
Its jobwrite tests + assertionsbuild, run, collect pass/fail
FormC macroscommand-line tool


How they talk to each other

Here's the whole machine — two tools, four steps:

The ztest + Twister loop: Twister builds and runs the test binary on the target, the binary prints fixed START/PASS/FAIL text to the console, Twister greps that text and produces a pass/fail report

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:

MacroOn 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.
  • Unitjust 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 / FAIL text 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