Zephyr Ztest

Zephyr ztest — The Complete Guide
Zephyr RTOS · SDK · Embedded Testing

Zephyr ztest

A complete, in-depth guide to unit and integration testing in the Zephyr RTOS SDK

#include <zephyr/ztest.h> ZTEST_SUITE zassert_* Fixtures FFF Mocking Twister Ztress

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.

ztest in the Zephyr Ecosystem
Developer writes ztest west build CMake + Kconfig Hardware / QEMU native_sim Twister Test Runner PASS / FAIL Reports + CI ztest.h + .c files CMakeLists.txt ELF binary + serial

Architecture & File Structure

A ztest project is a self-contained Zephyr application. It needs at minimum four files:

Required File Structure
tests/my_module/ CMakeLists.txt Declares project, links zephyr and adds test source files prj.conf Kconfig options e.g. CONFIG_ZTEST=y testcase.yaml Metadata for Twister: platform filters, test tags, harness type src/main.c Your test suites & test cases using ZTEST_SUITE / ZTEST macros

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.

ZTEST_SUITE Macro Arguments
ZTEST_SUITE( suite_name , predicate , setup , before , after , teardown ) name unique ID predicate when to run? setup once, returns fixture before before each test after after each test teardown once, at end Pass NULL for any optional argument you don't need
#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:

MacroDescriptionUse When
ZTEST(suite, name)Basic test, kernel thread contextMost common case
ZTEST_USER(suite, name)Runs in userspace thread if CONFIG_USERSPACE=yTesting userspace-restricted code
ZTEST_F(suite, name)Test receives a fixture pointer automaticallyWhen you need shared state/setup
ZTEST_USER_F(suite, name)Fixture + userspace combinedUserspace + shared state
ZTEST_P(suite, name)Parameterized test; receives data pointerRunning 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:

Three Families of Checks — What Happens on Failure
zassert_* Fail → stop test zexpect_* Fail → collect & continue zassume_* Fail → skip test

Common zassert_* Macros

MacroPasses 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().

Fixture Lifecycle Within a Suite
setup() alloc fixture once per suite run before() reset fixture before each test ZTEST_F() test_1 test_2 test_3 ... fixture auto-injected after() cleanup step after each test teardown() free fixture once per suite run
#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.

Complete Execution Order for a Suite with 3 Tests
ztest_run_all() — start setup() → alloc fixture test 1 before() ZTEST_F test_1() after() PASS / FAIL / SKIP test 2 before() ZTEST_F test_2() after() test 3 before() ZTEST_F test_3() after() teardown() → free fixture

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.

Real Code vs Mocked Dependency
WITHOUT MOCK — Slow, hardware needed Module under test Real GPIO Driver ← needs hardware WITH FFF MOCK — Fast, runs anywhere Module under test FAKE_VOID_FUNC( gpio_pin_set) gpio_pin_set_fake .call_count / .arg0_val
#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)");
}
Tip — Wrapper Pattern

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
Twister Report — Serial Output Format
*** Booting Zephyr OS build v3.6.0 *** Running TESTSUITE buffer_suite =================================================================== START - test_initial_state PASS - test_initial_state in 0.001 seconds START - test_write_and_read PASS - test_write_and_read in 0.002 seconds =================================================================== TESTSUITE buffer_suite succeeded

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);
}
When to use Ztress

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.

Summary

ztest at a Glance — Quick Reference
ZTEST MACRO CHEAT SHEET Suite Setup ZTEST_SUITE() Define a named test suite ZTEST_RULE() Global before/after hooks test_main() Custom entry point Test Macros ZTEST(suite, name) ZTEST_F(suite, name) ZTEST_USER(suite, name) ZTEST_P(suite, name) ZTEST_EXPECT_FAIL() ZTEST_EXPECT_SKIP() Checks zassert_*() fail + stop test zexpect_*() fail + continue zassume_*() skip test
Zephyr ztest — Complete Guide
Covers: Suites · Test Macros · Assertions · Fixtures · Lifecycle · FFF Mocking · Twister · Ztress
Source: docs.zephyrproject.org/latest/develop/test/ztest.html

Read more