Zephyr Ztest
Zephyr ztest
A complete, in-depth guide to unit and integration testing in the Zephyr RTOS SDK
Contents
What is Ztest?
The The Zephyr Test Framework (Ztest) is the official testing framework built into the Zephyr RTOS SDK. It provides a structured, macro-driven system for writing and running unit tests and integration tests directly on embedded targets or in QEMU/native simulators — without needing an external test harness.
It lives at #include <zephyr/ztest.h> and is tightly integrated with Zephyr's build system (west + CMake) and test runner (Twister). You write tests in C, build them like a normal Zephyr app, and get structured pass/fail output over serial.
Integration Testing
Test how multiple modules interact — timers, queues, drivers — running the full Zephyr kernel. Tests run on real hardware or QEMU.
Unit Testing
Test individual C modules in isolation. Build only the module under test, mock its dependencies, and run natively on your host machine for speed.
Architecture & File Structure
A ztest project is a self-contained Zephyr application. It needs at minimum four files:
Minimal CMakeLists.txt
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(my_module_test)
target_sources(app PRIVATE src/main.c)
Minimal prj.conf
# prj.conf — enable the ztest framework
CONFIG_ZTEST=y
CONFIG_ZTEST_NEW_API=y
Minimal testcase.yaml
# testcase.yaml — tells Twister what this test is
tests:
myapp.mymodule:
tags: mymodule
harness: ztest
Creating a Test Suite
A test suite is a named group of related test cases. You create one with the ZTEST_SUITE macro. Every suite has optional lifecycle hooks and an optional predicate to control when it runs.
#include <zephyr/ztest.h>
/* Optional: only run suite when state->phase == 1 */
static bool my_predicate(const void *state)
{
return ((const struct app_state *)state)->phase == 1;
}
/* The simplest possible suite — no lifecycle hooks */
ZTEST_SUITE(math_tests, NULL, NULL, NULL, NULL, NULL);
/* Suite that only runs based on global state */
ZTEST_SUITE(phase1_tests, my_predicate, NULL, NULL, NULL, NULL);
Adding Tests to a Suite
There are five macros for defining individual test cases. Choose based on whether you need fixtures, userspace execution, or parameters:
| Macro | Description | Use When |
|---|---|---|
| ZTEST(suite, name) | Basic test, kernel thread context | Most common case |
| ZTEST_USER(suite, name) | Runs in userspace thread if CONFIG_USERSPACE=y | Testing userspace-restricted code |
| ZTEST_F(suite, name) | Test receives a fixture pointer automatically | When you need shared state/setup |
| ZTEST_USER_F(suite, name) | Fixture + userspace combined | Userspace + shared state |
| ZTEST_P(suite, name) | Parameterized test; receives data pointer | Running same logic with different inputs |
#include <zephyr/ztest.h>
ZTEST_SUITE(math_tests, NULL, NULL, NULL, NULL, NULL);
/* Basic test */
ZTEST(math_tests, test_addition)
{
int result = add(3, 4);
zassert_equal(result, 7, "3 + 4 should be 7, got %d", result);
}
/* Userspace test — runs in unprivileged thread */
ZTEST_USER(math_tests, test_subtraction_user)
{
zassert_equal(subtract(10, 3), 7, NULL);
}
Assertions, Expectations & Assumptions
ztest provides three families of check macros. The difference is in what happens when they fail:
Common zassert_* Macros
| Macro | Passes When |
|---|---|
| zassert_true(cond, msg) | condition is true |
| zassert_false(cond, msg) | condition is false |
| zassert_equal(a, b, msg) | a == b |
| zassert_not_equal(a, b, msg) | a != b |
| zassert_equal_ptr(a, b, msg) | pointers are equal |
| zassert_is_null(ptr, msg) | ptr == NULL |
| zassert_not_null(ptr, msg) | ptr != NULL |
| zassert_mem_equal(a, b, size, msg) | memory regions match |
| zassert_within(a, b, delta, msg) | |a - b| <= delta |
| zassert_ok(rc, msg) | rc == 0 (no error) |
ZTEST(sensor_tests, test_temperature_read)
{
int rc;
struct sensor_value val;
/* Must succeed — fail fast if init fails */
rc = sensor_init(&dev);
zassert_ok(rc, "sensor_init failed: %d", rc);
/* Pointer must be valid */
zassert_not_null(&dev, "device handle is NULL");
rc = sensor_read(&dev, &val);
zassert_ok(rc, NULL);
/* Value should be within realistic range */
zassert_within(val.val1, 25, 10, "Temp %d out of range", val.val1);
}
ZTEST(sensor_tests, test_skip_if_unavailable)
{
/* Skip this test if no hardware sensor is present */
zassume_true(sensor_is_available(), "No sensor, skipping");
/* Only reached if sensor exists */
zassert_ok(sensor_calibrate(), NULL);
}
Test Fixtures
Fixtures let you share pre-allocated state across all tests in a suite without repeating setup code in each test. The fixture struct is created once in setup(), passed to before() for reset before each test, and freed in teardown().
#include <zephyr/ztest.h>
#include <stdlib.h>
/* 1. Define the fixture struct */
struct buffer_suite_fixture {
size_t max_size;
size_t size;
uint8_t *buf;
};
/* 2. Allocate once — setup runs once per suite run */
static void *buffer_suite_setup(void)
{
struct buffer_suite_fixture *f =
malloc(sizeof(struct buffer_suite_fixture));
zassume_not_null(f, NULL);
f->buf = malloc(256);
zassume_not_null(f->buf, NULL);
f->max_size = 256;
return f;
}
/* 3. Reset before every test */
static void buffer_suite_before(void *f)
{
struct buffer_suite_fixture *fixture = f;
memset(fixture->buf, 0, fixture->max_size);
fixture->size = 0;
}
/* 4. Free once — teardown runs at end of all suite tests */
static void buffer_suite_teardown(void *f)
{
struct buffer_suite_fixture *fixture = f;
free(fixture->buf);
free(fixture);
}
/* 5. Register the suite with hooks */
ZTEST_SUITE(buffer_suite, NULL,
buffer_suite_setup,
buffer_suite_before,
NULL, /* no after hook needed */
buffer_suite_teardown);
/* 6. Use ZTEST_F — fixture is injected as 'fixture' */
ZTEST_F(buffer_suite, test_initial_state)
{
zassert_equal(fixture->size, 0, "Size should start at 0");
zassert_equal(fixture->max_size, 256, NULL);
zassert_equal(fixture->buf[0], 0, "Buffer should be zeroed");
}
ZTEST_F(buffer_suite, test_write_and_read)
{
const char *msg = "hello";
memcpy(fixture->buf, msg, 5);
fixture->size = 5;
zassert_mem_equal(fixture->buf, msg, 5, "Buffer mismatch");
zassert_equal(fixture->size, 5, NULL);
}
Full Test Lifecycle
Understanding exactly when each hook fires helps to design clean, reproducible tests.
Advanced Features
Expected Failures & Skips
Some tests are intentionally broken — they test known-broken code or platform-specific behaviour. This is useful for tracking bugs that are not yet fixed. You can annotate them so Twister correctly marks them as passing:
ZTEST_SUITE(edge_cases, NULL, NULL, NULL, NULL, NULL);
/* Tell ztest this test is expected to fail */
ZTEST_EXPECT_FAIL(edge_cases, test_known_bug);
ZTEST(edge_cases, test_known_bug)
{
/* This intentionally fails — but is annotated, so suite reports PASS */
zassert_true(false, "Bug #1234 not yet fixed");
}
/* Tell ztest this test is expected to skip */
ZTEST_EXPECT_SKIP(edge_cases, test_hw_only);
ZTEST(edge_cases, test_hw_only)
{
zassume_true(false, "Requires physical ADC — skipping on sim");
}
If a test marked with ZTEST_EXPECT_FAIL actually passes, it will typically be reported as a failure (an "unexpected pass") to alert you that the issue may have been resolved.
Custom test_main
By default ztest auto-runs all suites. If you need to inject global state or control execution order, define test_main():
static struct app_state global_state = { .phase = 1 };
void test_main(void)
{
/* Runs all suites whose predicates accept &global_state */
ztest_run_all(&global_state,
false, /* shuffle = false */
1, /* run each suite once */
1); /* run each test once */
}
Shuffling & Repeating
You can shuffle test order (to catch order-dependent failures) or repeat tests (to catch flakiness) at build time via Kconfig or at runtime via ztest_run_all() parameters:
# prj.conf — shuffle test execution order
CONFIG_ZTEST_SHUFFLE=y
CONFIG_ZTEST_SHUFFLE_SUITE_REPEAT_COUNT=3
CONFIG_ZTEST_SHUFFLE_TEST_REPEAT_COUNT=2
Test Rules — Global Hooks
Test rules let you attach logic that runs before/after every single test across all suites — useful for system-wide invariant checking:
static void check_heap_on_start(const struct ztest_unit_test *test, void *data)
{
zassert_true(heap_is_valid(), "Heap corrupted before %s", test->name);
}
static void check_heap_on_end(const struct ztest_unit_test *test, void *data)
{
zassert_true(heap_is_valid(), "Heap corrupted after %s", test->name);
}
ZTEST_RULE(heap_integrity, check_heap_on_start, check_heap_on_end);
Mocking with FFF
Zephyr integrates the Fake Function Framework (FFF) for creating mock/stub functions. This lets you isolate the module under test from hardware drivers, kernel APIs, or other dependencies.
#include <zephyr/ztest.h>
#include <zephyr/fff.h>
/* Declare the FFF global state */
DEFINE_FFF_GLOBALS;
/* Create a fake for a void function with 3 args */
FAKE_VOID_FUNC(gpio_pin_set, const struct device *, gpio_pin_t, int);
/* Create a fake for a function returning int */
FAKE_VALUE_FUNC(int, gpio_pin_get, const struct device *, gpio_pin_t);
ZTEST_SUITE(gpio_tests, NULL, NULL, NULL, NULL, NULL);
ZTEST(gpio_tests, test_led_toggle)
{
/* Reset fake state before each use */
RESET_FAKE(gpio_pin_set);
RESET_FAKE(gpio_pin_get);
/* Configure fake return value */
gpio_pin_get_fake.return_val = 0; /* LED is currently OFF */
/* Call the function under test */
led_toggle(LED_RED);
/* Assert the fake was called correctly */
zassert_equal(gpio_pin_set_fake.call_count, 1,
"gpio_pin_set should be called once");
zassert_equal(gpio_pin_set_fake.arg2_val, 1,
"LED should be turned ON (arg2 = 1)");
}
If you need to mock a kernel function like k_msgq_put(), wrap it first: create msgq_wrapper_put() in your app code that calls through to the real function. In tests you can then fake the wrapper without touching kernel internals.
Running Tests with Twister
Twister is Zephyr's automated test runner. It discovers testcase.yaml files, builds the tests for the requested platforms, flashes or emulates them, collects serial output, and generates structured reports.
# Run all ztest tests on native_sim (fast, no hardware)
$ west twister -p native_sim -T tests/
# Run a specific test directory
$ west twister -p native_sim -T tests/my_module/
# Run on real hardware (nRF52840 DK)
$ west twister -p nrf52840dk/nrf52840 -T tests/ --device-testing
# Run with verbose output
$ west twister -p native_sim -T tests/ -v
# Filter by tag
$ west twister -p native_sim -T tests/ --tag mymodule
testcase.yaml Reference
tests:
myapp.sensor.unit:
tags: sensor unit
harness: ztest
harness_config:
type: one_line
regex:
- "TESTSUITE sensor_tests succeeded"
platform_allow:
- native_sim
- nrf52840dk/nrf52840
filter: CONFIG_SENSOR # only build if Kconfig enables sensor
timeout: 30
Stress Testing with Ztress
The Ztress framework is a companion to ztest for stress testing concurrent code. It runs user functions simultaneously across multiple thread priority levels, deliberately maximising preemptions to expose race conditions and deadlocks.
#include <zephyr/ztest.h>
#include <zephyr/ztress.h>
static bool producer(void *user_data, uint32_t cnt, bool last, uint32_t prio)
{
struct k_msgq *q = user_data;
uint32_t val = cnt;
return k_msgq_put(q, &val, K_NO_WAIT) == 0;
}
static bool consumer(void *user_data, uint32_t cnt, bool last, uint32_t prio)
{
struct k_msgq *q = user_data;
uint32_t val;
return k_msgq_get(q, &val, K_NO_WAIT) == 0;
}
ZTEST(stress_tests, test_msgq_under_load)
{
ZTRESS_EXECUTE(
ZTRESS_THREAD(producer, &my_queue, 1000, 0, Z_TIMEOUT_MS(5000)),
ZTRESS_THREAD(consumer, &my_queue, 900, 0, Z_TIMEOUT_MS(5000))
);
/* Verify no messages were lost */
zassert_equal(k_msgq_num_used_get(&my_queue), 0, NULL);
}
Use Ztress when validating thread-safe queues, ring buffers, semaphores, or any shared data structure. It's especially valuable for validating RTOS primitives before shipping firmware for high-reliability applications.