develop/test/ztest.html · develop/test/ztest.rst 공식 원문 ↗

Zephyr 3.7.0 · Develop · Testing · Ztest

Ztest test framework

Integration과 unit test를 같은 suite 구조로 작성하고 assertion·fixture·mock·stress context로 검증합니다.

Official pathdevelop/test/ztest.html
Source filedevelop/test/ztest.rst
Source versionZephyr 3.7.0
TranslationFull · reviewed

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

Part 1

요약·해설

ZTEST_SUITE lifecycle, testcase·fixture·expected result·rule·custom main, Twister integration, unit test, traceability, Ztress, assertion·expectation·assumption, output·shuffle·selection과 FFF extension을 설명합니다.

Structure

suite · test · fixture

Result

assert · expect · assume

Advanced

Ztress · FFF · shuffle

Part 2

접을 수 있는 영어 원문 전체

영어 원문 전체 펼치기
원문 SHA-256 867FCC2BD809625EAC8904C8F7D4D4AB73B2A1001038098F2E719594B0913201
.. _test-framework:

Test Framework
###############

The Zephyr Test Framework (Ztest) provides a simple testing framework intended
to be used during development.  It provides basic assertion macros and a generic
test structure.

The framework can be used in two ways, either as a generic framework for
integration testing, or for unit testing specific modules.

Creating a test suite
*********************

Using Ztest to create a test suite is as easy as calling the :c:macro:`ZTEST_SUITE`. The macro
accepts the following arguments:

* ``suite_name`` - The name of the suite. This name must be unique within a single binary.
* :c:type:`ztest_suite_predicate_t` - An optional predicate function to allow choosing when the
  test will run. The predicate will get a pointer to the global state passed in through
  :c:func:`ztest_run_all` and should return a boolean to decide if the suite should run.
* :c:type:`ztest_suite_setup_t` - An optional setup function which returns a test fixture. This
  will be called and run once per test suite run.
* :c:type:`ztest_suite_before_t` - An optional before function which will run before every single
  test in this suite.
* :c:type:`ztest_suite_after_t` - An optional after function which will run after every single
  test in this suite.
* :c:type:`ztest_suite_teardown_t` - An optional teardown function which will run at the end of
  all the tests in the suite.

Below is an example of a test suite using a predicate:

.. code-block:: C

   #include <zephyr/ztest.h>
   #include "test_state.h"

   static bool predicate(const void *global_state)
   {
   	return ((const struct test_state*)global_state)->x == 5;
   }

   ZTEST_SUITE(alternating_suite, predicate, NULL, NULL, NULL, NULL);

Adding tests to a suite
***********************

There are 4 macros used to add a test to a suite, they are:

* :c:macro:`ZTEST` ``(suite_name, test_name)`` - Which can be used to add a test by ``test_name`` to a
  given suite by ``suite_name``.
* :c:macro:`ZTEST_USER` ``(suite_name, test_name)`` - Which behaves the same as :c:macro:`ZTEST`, only
  that when :kconfig:option:`CONFIG_USERSPACE` is enabled, then the test will be run in a userspace
  thread.
* :c:macro:`ZTEST_F` ``(suite_name, test_name)`` - Which behaves the same as :c:macro:`ZTEST`, only
  that the test function will already include a variable named ``fixture`` with the type
  ``<suite_name>_fixture``.
* :c:macro:`ZTEST_USER_F` ``(suite_name, test_name)`` - Which combines the fixture feature of
  :c:macro:`ZTEST_F` with the userspace threading for the test.

Test fixtures
=============

Test fixtures can be used to help simplify repeated test setup operations. In many cases, tests in
the same suite will require some initial setup followed by some form of reset between each test.
This is achieved via fixtures in the following way:

.. code-block:: C

   #include <zephyr/ztest.h>

   struct my_suite_fixture {
   	size_t max_size;
   	size_t size;
   	uint8_t buff[1];
   };

   static void *my_suite_setup(void)
   {
   	/* Allocate the fixture with 256 byte buffer */
      struct my_suite_fixture *fixture = malloc(sizeof(struct my_suite_fixture) + 255);

   	zassume_not_null(fixture, NULL);
   	fixture->max_size = 256;

   	return fixture;
   }

   static void my_suite_before(void *f)
   {
   	struct my_suite_fixture *fixture = (struct my_suite_fixture *)f;
   	memset(fixture->buff, 0, fixture->max_size);
   	fixture->size = 0;
   }

   static void my_suite_teardown(void *f)
   {
      free(f);
   }

   ZTEST_SUITE(my_suite, NULL, my_suite_setup, my_suite_before, NULL, my_suite_teardown);

   ZTEST_F(my_suite, test_feature_x)
   {
   	zassert_equal(0, fixture->size);
   	zassert_equal(256, fixture->max_size);
   }

Using memory allocated by a test fixture in a userspace thread, such as during execution of
:c:macro:`ZTEST_USER` or :c:macro:`ZTEST_USER_F`, requires that memory to be declared userspace
accessible. This is because the fixture memory is owned and initialized by kernel space. The Ztest
framework provides the :c:macro:`ZTEST_DMEM` and :c:macro:`ZTEST_BMEM` macros for use of such
user/kernel space shared memory.

Advanced features
*****************

Test result expectations
========================

Some tests were made to be broken. In cases where the test is expected to fail or skip due to the
nature of the code, it's possible to annotate the test as such. For example:

  .. code-block:: C

    #include <zephyr/ztest.h>

    ZTEST_SUITE(my_suite, NULL, NULL, NULL, NULL, NULL);

    ZTEST_EXPECT_FAIL(my_suite, test_fail);
    ZTEST(my_suite, test_fail)
    {
      /** This will fail the test */
      zassert_true(false, NULL);
    }

    ZTEST_EXPECT_SKIP(my_suite, test_skip);
    ZTEST(my_suite, test_skip)
    {
      /** This will skip the test */
      zassume_true(false, NULL);
    }

In this example, the above tests should be marked as failed and skipped respectively. Instead,
Ztest will mark both as passed due to the expectation.

Test rules
==========

Test rules are a way to run the same logic for every test and every suite. There are a lot of cases
where you might want to reset some state for every test in the binary (regardless of which suite is
currently running). As an example, this could be to reset mocks, reset emulators, flush the UART,
etc.:

.. code-block:: C

   #include <zephyr/fff.h>
   #include <zephyr/ztest.h>

   #include "test_mocks.h"

   DEFINE_FFF_GLOBALS;

   DEFINE_FAKE_VOID_FUN(my_weak_func);

   static void fff_reset_rule_before(const struct ztest_unit_test *test, void *fixture)
   {
   	ARG_UNUSED(test);
   	ARG_UNUSED(fixture);

   	RESET_FAKE(my_weak_func);
   }

   ZTEST_RULE(fff_reset_rule, fff_reset_rule_before, NULL);

A custom ``test_main``
======================

While the Ztest framework provides a default :c:func:`test_main` function, it's possible that some
applications will want to provide custom behavior. This is particularly true if there's some global
state that the tests depend on and that state either cannot be replicated or is difficult to
replicate without starting the process over. For example, one such state could be a power sequence.
Assuming there's a board with several steps in the power-on sequence a test suite can be written
using the ``predicate`` to control when it would run. In that case, the :c:func:`test_main`
function can be written as follows:

.. code-block:: C

   #include <zephyr/ztest.h>

   #include "my_test.h"

   void test_main(void)
   {
        struct power_sequence_state state;

        /* Only suites that use a predicate checking for phase == PWR_PHASE_0 will run. */
        state.phase = PWR_PHASE_0;
        ztest_run_all(&state, false, 1, 1);

        /* Only suites that use a predicate checking for phase == PWR_PHASE_1 will run. */
        state.phase = PWR_PHASE_1;
        ztest_run_all(&state, false, 1, 1);

        /* Only suites that use a predicate checking for phase == PWR_PHASE_2 will run. */
        state.phase = PWR_PHASE_2;
        ztest_run_all(&state, false, 1, 1);

        /* Check that all the suites in this binary ran at least once. */
        ztest_verify_all_test_suites_ran();
   }


Quick start - Integration testing
*********************************

A simple working base is located at :zephyr_file:`samples/subsys/testsuite/integration`.
To make a test application for the **bar** component of **foo**, you should copy the
sample folder to ``tests/foo/bar`` and edit files there adjusting for your test
application's purposes.

To build and execute all applicable test scenarios defined in your test application
use the :ref:`Twister <twister_script>` tool, for example:

.. code-block:: console

    ./scripts/twister -T tests/foo/bar/

To select just one of the test scenarios, run Twister with ``--scenario`` command:

.. code-block:: console

   ./scripts/twister --scenario tests/foo/bar/your.test.scenario.name

In the command line above ``tests/foo/bar`` is the path to your test application and
``your.test.scenario.name`` references a test scenario defined in :file:`testcase.yaml`
file, which is like ``sample.testing.ztest`` in the boilerplate test suite sample.

See :ref:`Twister test project diagram <twister_test_project_diagram>` for more details
on how Twister deals with Ztest application.

The sample contains the following files:

CMakeLists.txt

.. literalinclude:: ../../../../../samples/subsys/testsuite/integration/CMakeLists.txt
   :language: CMake
   :linenos:

testcase.yaml

.. literalinclude:: ../../../../../samples/subsys/testsuite/integration/testcase.yaml
   :language: yaml
   :linenos:

prj.conf

.. literalinclude:: ../../../../../samples/subsys/testsuite/integration/prj.conf
   :language: text
   :linenos:

src/main.c (see :ref:`best practices <main_c_bp>`)

.. literalinclude:: ../../../../../samples/subsys/testsuite/integration/src/main.c
   :language: c
   :linenos:

.. contents::
   :depth: 1
   :local:
   :backlinks: top



A test application may consist of multiple test suites that
either can be testing functionality or APIs. Functions implementing a test case
should follow the guidelines below:

* Test cases function names should be prefixed with **test_**
* Test cases should be documented using doxygen
* Test case function names should be unique within the section or component being
  tested

For example:

.. code-block:: C

   /**
    * @brief Test Asserts
    *
    * This test case verifies the zassert_true macro.
    */
   ZTEST(my_suite, test_assert)
   {
           zassert_true(1, "1 was false");
   }

Listing Tests
=============

Tests (test applications) in the Zephyr tree consist of many test scenarios that run as
part of a project and test similar functionality, for example an API or a
feature. The ``twister`` script can parse the test scenarios, suites and cases in all
test applications or a subset of them, and can generate reports on a granular
level, i.e. if test cases have passed or failed or if they were blocked or skipped.

Twister parses the source files looking for test case names, so you
can list all kernel test cases, for example, by running:

.. code-block:: console

   ./scripts/twister --list-tests -T tests/kernel

Skipping Tests
==============

Special- or architecture-specific tests cannot run on all
platforms and architectures, however we still want to count those and
report them as being skipped.  Because the test inventory and
the list of tests is extracted from the code, adding
conditionals inside the test suite is sub-optimal.  Tests that need
to be skipped for a certain platform or feature need to explicitly
report a skip using :c:func:`ztest_test_skip` or :c:macro:`Z_TEST_SKIP_IFDEF`. If the test runs,
it needs to report either a pass or fail.  For example:

.. code-block:: C

   #ifdef CONFIG_TEST1
   ZTEST(common, test_test1)
   {
   	zassert_true(1, "true");
   }
   #else
   ZTEST(common, test_test1)
   {
   	ztest_test_skip();
   }
   #endif

   ZTEST(common, test_test2)
   {
   	Z_TEST_SKIP_IFDEF(CONFIG_BUGxxxxx);
   	zassert_equal(1, 0, NULL);
   }

   ZTEST_SUITE(common, NULL, NULL, NULL, NULL, NULL);

.. _ztest_unit_testing:

Quick start - Unit testing
**************************

Ztest can be used for unit testing. This means that rather than including the
entire Zephyr OS for testing a single function, you can focus the testing
efforts into the specific module in question. This will speed up testing since
only the module will have to be compiled in, and the tested functions will be
called directly.

Examples of unit tests can be found in the :zephyr_file:`tests/unit/` folder.
In order to declare the unit tests present in a source folder, you need to add
the relevant source files to the ``testbinary`` target from the CMake
:zephyr_file:`unittest <cmake/modules/unittest.cmake>` component. See a minimal
example below:

.. code-block:: cmake

   cmake_minimum_required(VERSION 3.20.0)

   project(app)
   find_package(Zephyr COMPONENTS unittest REQUIRED HINTS $ENV{ZEPHYR_BASE})
   target_sources(testbinary PRIVATE main.c)

Since you won't be including basic kernel data structures that most code
depends on, you have to provide function stubs in the test. Ztest provides
some helpers for mocking functions, as demonstrated below.

In a unit test, mock objects can simulate the behavior of complex real objects
and are used to decide whether a test failed or passed by verifying whether an
interaction with an object occurred, and if required, to assert the order of
that interaction.

Best practices for declaring the test suite
*******************************************

*twister* and other validation tools need to obtain the list of
test cases that a Zephyr *ztest* test image will expose.

.. admonition:: Rationale

   This all is for the purpose of traceability. It's not enough to
   have only a semaphore test application.  We also need to show that we
   have testpoints for all APIs and functionality, and we trace back
   to documentation of the API, and functional requirements.

   The idea is that test reports show results for every test case
   as passed, failed, blocked, or skipped.  Reporting on only the
   high-level test application, particularly when tests do too
   many things, is too vague.

Other questions:

- Why not pre-scan with CPP and then parse? or post scan the ELF file?

  If C pre-processing or building fails because of any issue, then we
  won't be able to tell the subcases.

- Why not declare them in the YAML test configuration?

  A separate test case description file would be harder to maintain
  than just keeping the information in the test source files
  themselves -- only one file to update when changes are made
  eliminates duplication.

Stress test framework
*********************

Zephyr stress test framework (Ztress) provides an environment for executing user
functions in multiple priority contexts. It can be used to validate that code is
resilient to preemptions. The framework tracks the number of executions and preemptions
for each context. Execution can have various completion conditions like timeout,
number of executions or number of preemptions.

The framework is setting up the environment by creating the requested number of threads
(each on different priority), optionally starting a timer. For each context, a user
function (different for each context) is called and then the context sleeps for
a randomized amount of system ticks. The framework is tracking CPU load and adjusts sleeping
periods to achieve higher CPU load. In order to increase the probability of preemptions,
the system clock frequency should be relatively high. The default 100 Hz on QEMU x86
is much too low and it is recommended to increase it to 100 kHz.

The stress test environment is setup and executed using :c:macro:`ZTRESS_EXECUTE` which
accepts a variable number of arguments. Each argument is a context that is
specified by :c:macro:`ZTRESS_TIMER` or :c:macro:`ZTRESS_THREAD` macros. Contexts
are specified in priority descending order. Each context specifies completion
conditions by providing the minimum number of executions and preemptions. When all
conditions are met and the execution has completed, an execution report is printed
and the macro returns. Note that while the test is executing, a progress report is
periodically printed.

Execution can be prematurely completed by specifying a test timeout (:c:func:`ztress_set_timeout`)
or an explicit abort (:c:func:`ztress_abort`).

User function parameters contains an execution counter and a flag indicating if it is
the last execution.

The example below presents how to setup and run 3 contexts (one of which is k_timer
interrupt handler context). Completion criteria is set to at least 10000 executions
of each context and 1000 preemptions of the lowest priority context. Additionally,
the timeout is configured to complete after 10 seconds if those conditions are not met.
The last argument of each context is the initial sleep time which will be adjusted throughout
the test to achieve the highest CPU load.

  .. code-block:: C

             ztress_set_timeout(K_MSEC(10000));
             ZTRESS_EXECUTE(ZTRESS_TIMER(foo_0, user_data_0, 10000, Z_TIMEOUT_TICKS(20)),
                            ZTRESS_THREAD(foo_1, user_data_1, 10000, 0, Z_TIMEOUT_TICKS(20)),
                            ZTRESS_THREAD(foo_2, user_data_2, 10000, 1000, Z_TIMEOUT_TICKS(20)));

Configuration
=============

Static configuration of Ztress contains:

 - :kconfig:option:`CONFIG_ZTRESS_MAX_THREADS` - number of supported threads.
 - :kconfig:option:`CONFIG_ZTRESS_STACK_SIZE` - Stack size of created threads.
 - :kconfig:option:`CONFIG_ZTRESS_REPORT_PROGRESS_MS` - Test progress report interval.

API reference
*************

Running tests
=============

.. doxygengroup:: ztest_test

Assertions
==========

These macros will instantly fail the test if the related assertion fails.
When an assertion fails, it will print the current file, line and function,
alongside a reason for the failure and an optional message. If the config
:kconfig:option:`CONFIG_ZTEST_ASSERT_VERBOSE` is 0, the assertions will only print the
file and line numbers, reducing the binary size of the test.

Example output for a failed macro from
``zassert_equal(buf->ref, 2, "Invalid refcount")``:

.. code-block:: none

    Assertion failed at main.c:62: test_get_single_buffer: Invalid refcount (buf->ref not equal to 2)
    Aborted at unit test function

.. doxygengroup:: ztest_assert


Expectations
============

These macros will continue test execution if the related expectation fails and subsequently fail the
test at the end of its execution.  When an expectation fails, it will print the current file, line,
and function, alongside a reason for the failure and an optional message but continue executing the
test. If the config :kconfig:option:`CONFIG_ZTEST_ASSERT_VERBOSE` is 0, the expectations will only print the
file and line numbers, reducing the binary size of the test.

For example, if the following expectations fail:

.. code-block:: C

   zexpect_equal(buf->ref, 2, "Invalid refcount");
   zexpect_equal(buf->ref, 1337, "Invalid refcount");

The output will look something like:

.. code-block:: none

   START - test_get_single_buffer
       Expectation failed at main.c:62: test_get_single_buffer: Invalid refcount (buf->ref not equal to 2)
       Expectation failed at main.c:63: test_get_single_buffer: Invalid refcount (buf->ref not equal to 1337)
    FAIL - test_get_single_buffer in 0.0 seconds

.. doxygengroup:: ztest_expect

Assumptions
===========

These macros will instantly skip the test or suite if the related assumption fails.
When an assumption fails, it will print the current file, line, and function,
alongside a reason for the failure and an optional message. If the config
:kconfig:option:`CONFIG_ZTEST_ASSERT_VERBOSE` is 0, the assumptions will only print the
file and line numbers, reducing the binary size of the test.

Example output for a failed macro from
``zassume_equal(buf->ref, 2, "Invalid refcount")``:

.. code-block::none

    START - test_get_single_buffer
        Assumption failed at main.c:62: test_get_single_buffer: Invalid refcount (buf->ref not equal to 2)
     SKIP - test_get_single_buffer in 0.0 seconds

.. doxygengroup:: ztest_assume


Ztress
======

.. doxygengroup:: ztest_ztress


.. _mocking-fff:

Mocking via FFF
===============

Zephyr has integrated with FFF for mocking. See `FFF`_ for documentation. To use it,
include the relevant header:

.. code-block:: C

   #include <zephyr/fff.h>

Zephyr provides several FFF-based fake drivers which can be used as either stubs or mocks. Fake
driver instances are configured via :ref:`devicetree` and :ref:`kconfig`. See the following
devicetree bindings for more information:

 - :dtcompatible:`zephyr,fake-can`
 - :dtcompatible:`zephyr,fake-eeprom`

Zephyr also has defined extensions to FFF for simplified declarations of fake functions.
See :ref:`FFF Extensions <fff-extensions>`.

Customizing Test Output
***********************
Customization is enabled by setting :kconfig:option:`CONFIG_ZTEST_TC_UTIL_USER_OVERRIDE` to "y"
and adding a file :file:`tc_util_user_override.h` with your overrides.

Add the line ``zephyr_include_directories(my_folder)`` to
your project's :file:`CMakeLists.txt` to let Zephyr find your header file during builds.

See the file :zephyr_file:`subsys/testsuite/include/zephyr/tc_util.h` to see
which macros and/or defines can be overridden.
These will be surrounded by blocks such as:

.. code-block:: C

   #ifndef SOMETHING
   #define SOMETHING <default implementation>
   #endif /* SOMETHING */

.. _ztest_shuffle:

Shuffling Test Sequence
***********************
By default the tests are sorted and ran in alphanumerical order.  Test cases may
be dependent on this sequence. Enable :kconfig:option:`CONFIG_ZTEST_SHUFFLE` to
randomize the order. The output from the test will display the seed for failed
tests.  For native simulator builds you can provide the seed as an argument to
twister with `--seed`

Static configuration of ZTEST_SHUFFLE contains:

 - :kconfig:option:`CONFIG_ZTEST_SHUFFLE_SUITE_REPEAT_COUNT` - Number of iterations the test suite will run.
 - :kconfig:option:`CONFIG_ZTEST_SHUFFLE_TEST_REPEAT_COUNT` - Number of iterations the test will run.


Test Selection
**************
For tests built for native simulator, use command line arguments to list
or select tests to run. The test argument expects a comma separated list
of ``suite::test`` .  You can substitute the test name with an ``*`` to run all
tests within a suite.

For example

.. code-block:: bash

    $ zephyr.exe -list
    $ zephyr.exe -test="fixture_tests::test_fixture_pointer,framework_tests::test_assert_mem_equal"
    $ zephyr.exe -test="framework_tests::*"


.. _fff-extensions:

FFF Extensions
**************

.. doxygengroup:: fff_extensions


.. _FFF: https://github.com/meekrosoft/fff

Part 3

한국어 전문 번역

Test framework

Zephyr Test Framework(Ztest)는 개발 과정에서 사용할 단순한 test framework입니다. 기본 assertion macro와 일반화된 test 구조를 제공합니다.

전체 Zephyr application을 대상으로 하는 integration test framework로도, 특정 module을 직접 검증하는 unit test framework로도 사용할 수 있습니다.

Test suite 만들기

ZTEST_SUITE macro로 suite를 선언합니다. 하나의 binary 안에서 suite name은 유일해야 하며 다음 argument를 받습니다.

  • suite_name: suite의 고유 이름
  • ztest_suite_predicate_t: ztest_run_all이 전달한 global state를 받아 suite 실행 여부를 boolean으로 결정하는 optional predicate
  • ztest_suite_setup_t: suite run마다 한 번 호출되어 fixture를 반환하는 optional setup
  • ztest_suite_before_t: suite의 각 test 직전에 실행하는 optional before callback
  • ztest_suite_after_t: suite의 각 test 직후에 실행하는 optional after callback
  • ztest_suite_teardown_t: suite의 모든 test가 끝난 뒤 실행하는 optional teardown callback

Global state의 x가 5일 때만 실행하는 suite 예:

#include <zephyr/ztest.h>
#include "test_state.h"

static bool predicate(const void *global_state)
{
     return ((const struct test_state*)global_state)->x == 5;
}

ZTEST_SUITE(alternating_suite, predicate, NULL, NULL, NULL, NULL);

Suite에 test 추가

다음 네 macro로 suite에 test를 등록합니다.

  • ZTEST(suite_name, test_name): 지정 suite에 일반 test를 추가합니다.
  • ZTEST_USER(suite_name, test_name): CONFIG_USERSPACE가 활성화되면 userspace thread에서 실행합니다.
  • ZTEST_F(suite_name, test_name): test function에 <suite_name>_fixture type의 fixture variable을 자동 제공합니다.
  • ZTEST_USER_F(suite_name, test_name): fixture와 userspace thread 기능을 결합합니다.

Test fixture

같은 suite의 test가 공통 초기화와 testcase 사이 reset을 반복할 때 fixture가 중복 setup code를 줄입니다. Setup은 fixture memory를 준비하고 before는 testcase마다 상태를 초기화하며 teardown은 suite가 끝날 때 memory를 해제합니다.

#include <zephyr/ztest.h>

struct my_suite_fixture {
     size_t max_size;
     size_t size;
     uint8_t buff[1];
};

static void *my_suite_setup(void)
{
     /* Allocate the fixture with 256 byte buffer */
   struct my_suite_fixture *fixture = malloc(sizeof(struct my_suite_fixture) + 255);

     zassume_not_null(fixture, NULL);
     fixture->max_size = 256;

     return fixture;
}

static void my_suite_before(void *f)
{
     struct my_suite_fixture *fixture = (struct my_suite_fixture *)f;
     memset(fixture->buff, 0, fixture->max_size);
     fixture->size = 0;
}

static void my_suite_teardown(void *f)
{
   free(f);
}

ZTEST_SUITE(my_suite, NULL, my_suite_setup, my_suite_before, NULL, my_suite_teardown);

ZTEST_F(my_suite, test_feature_x)
{
     zassert_equal(0, fixture->size);
     zassert_equal(256, fixture->max_size);
}

ZTEST_USER 또는 ZTEST_USER_F의 userspace thread에서 fixture가 할당한 memory를 사용하려면 userspace 접근 가능 memory로 선언해야 합니다. Fixture memory는 kernel space가 소유·초기화하기 때문입니다. Ztest는 user/kernel shared memory용 ZTEST_DMEM과 ZTEST_BMEM을 제공합니다.

고급 기능

예상 test 결과

Code 성격상 실패하거나 skip해야 하는 test는 그 결과를 expected result로 annotation할 수 있습니다.

#include <zephyr/ztest.h>

ZTEST_SUITE(my_suite, NULL, NULL, NULL, NULL, NULL);

ZTEST_EXPECT_FAIL(my_suite, test_fail);
ZTEST(my_suite, test_fail)
{
  /** This will fail the test */
  zassert_true(false, NULL);
}

ZTEST_EXPECT_SKIP(my_suite, test_skip);
ZTEST(my_suite, test_skip)
{
  /** This will skip the test */
  zassume_true(false, NULL);
}

ZTEST_EXPECT_FAIL과 ZTEST_EXPECT_SKIP을 선언하면 실제 결과는 fail·skip이지만 expectation과 일치하므로 Ztest report에서는 두 test 모두 pass로 처리합니다.

Test rule

Test rule은 binary의 모든 suite와 test에 같은 logic을 적용합니다. 현재 suite와 관계없이 매 test마다 mock·emulator를 reset하거나 UART를 flush해야 할 때 사용합니다.

#include <zephyr/fff.h>
#include <zephyr/ztest.h>

#include "test_mocks.h"

DEFINE_FFF_GLOBALS;

DEFINE_FAKE_VOID_FUN(my_weak_func);

static void fff_reset_rule_before(const struct ztest_unit_test *test, void *fixture)
{
     ARG_UNUSED(test);
     ARG_UNUSED(fixture);

     RESET_FAKE(my_weak_func);
}

ZTEST_RULE(fff_reset_rule, fff_reset_rule_before, NULL);

ZTEST_RULE(name, before_each_fn, after_each_fn)은 공통 before/after callback을 등록합니다. 예제는 FFF fake function 상태를 testcase마다 reset합니다.

Custom test_main

Ztest는 기본 test_main을 제공하지만 process를 다시 시작하지 않고 재현하기 어려운 global state에 따라 suite를 여러 단계로 실행해야 하면 custom function을 정의할 수 있습니다.

전원 sequence 단계마다 predicate를 만족하는 suite만 실행하는 예:

#include <zephyr/ztest.h>

#include "my_test.h"

void test_main(void)
{
     struct power_sequence_state state;

     /* Only suites that use a predicate checking for phase == PWR_PHASE_0 will run. */
     state.phase = PWR_PHASE_0;
     ztest_run_all(&state, false, 1, 1);

     /* Only suites that use a predicate checking for phase == PWR_PHASE_1 will run. */
     state.phase = PWR_PHASE_1;
     ztest_run_all(&state, false, 1, 1);

     /* Only suites that use a predicate checking for phase == PWR_PHASE_2 will run. */
     state.phase = PWR_PHASE_2;
     ztest_run_all(&state, false, 1, 1);

     /* Check that all the suites in this binary ran at least once. */
     ztest_verify_all_test_suites_ran();
}

ztest_run_all에 phase state를 전달해 각 단계의 suite를 실행하고 마지막에 ztest_verify_all_test_suites_ran()로 binary의 모든 suite가 한 번 이상 실행됐는지 검증합니다.

빠른 시작 - Integration test

samples/subsys/testsuite/integration에 동작하는 기본 sample이 있습니다. foo component의 bar test application을 만들려면 이 folder를 tests/foo/bar로 복사한 뒤 목적에 맞게 수정합니다.

Application에 정의된 적용 가능한 scenario 전체를 Twister로 build·실행합니다.

./scripts/twister -T tests/foo/bar/

--scenario로 scenario 하나만 선택합니다.

./scripts/twister --scenario tests/foo/bar/your.test.scenario.name

tests/foo/bar는 test application path이고 your.test.scenario.name은 testcase.yaml에 정의한 scenario입니다. Boilerplate sample에서는 sample.testing.ztest에 해당합니다. Twister가 project를 다루는 방식은 Twister test project를 참조하십시오.

Sample CMakeLists.txt:

# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(integration)

FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})

Sample testcase.yaml:

tests:
  # section.subsection
  sample.testing.ztest:
    build_only: true
    platform_allow:
      - native_posix
      - native_sim
    integration_platforms:
      - native_sim
    tags: test_framework

Sample prj.conf:

CONFIG_ZTEST=y

Sample src/main.c:

/*
 * Copyright (c) 2016 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/ztest.h>


ZTEST_SUITE(framework_tests, NULL, NULL, NULL, NULL, NULL);

/**
 * @brief Test Asserts
 *
 * This test verifies various assert macros provided by ztest.
 *
 */
ZTEST(framework_tests, test_assert)
{
	zassert_true(1, "1 was false");
	zassert_false(0, "0 was true");
	zassert_is_null(NULL, "NULL was not NULL");
	zassert_not_null("foo", "\"foo\" was NULL");
	zassert_equal(1, 1, "1 was not equal to 1");
	zassert_equal_ptr(NULL, NULL, "NULL was not equal to NULL");
}

Test application에는 기능 또는 API를 검증하는 suite를 여러 개 둘 수 있습니다. Testcase function은 다음 지침을 따릅니다.

  • Function 이름을 test_로 시작합니다.
  • Doxygen으로 testcase를 문서화합니다.
  • 검사하는 section 또는 component 안에서 function 이름이 유일해야 합니다.

Assertion을 검증하는 문서화된 testcase 예:

/**
 * @brief Test Asserts
 *
 * This test case verifies the zassert_true macro.
 */
ZTEST(my_suite, test_assert)
{
        zassert_true(1, "1 was false");
}

Test 목록 표시

Zephyr tree의 test application은 관련 API·기능을 검증하는 많은 scenario를 포함합니다. Twister는 전체 또는 일부 application에서 scenario, suite, testcase를 parsing해 testcase 단위 pass·fail·blocked·skip report를 만들 수 있습니다.

Source file에서 testcase 이름을 찾아 kernel test 전체를 표시합니다.

./scripts/twister --list-tests -T tests/kernel

Test 건너뛰기

특정 platform·architecture에서 실행할 수 없는 test도 inventory에 포함하고 skip으로 보고해야 합니다. Source에서 test 목록을 추출하므로 preprocessor 조건만으로 testcase 자체를 숨기는 방식은 적합하지 않습니다.

ztest_test_skip() 또는 Z_TEST_SKIP_IFDEF로 명시적으로 skip을 보고하십시오. 실제로 실행된 test는 pass 또는 fail을 보고해야 합니다.

#ifdef CONFIG_TEST1
ZTEST(common, test_test1)
{
     zassert_true(1, "true");
}
#else
ZTEST(common, test_test1)
{
     ztest_test_skip();
}
#endif

ZTEST(common, test_test2)
{
     Z_TEST_SKIP_IFDEF(CONFIG_BUGxxxxx);
     zassert_equal(1, 0, NULL);
}

ZTEST_SUITE(common, NULL, NULL, NULL, NULL, NULL);

빠른 시작 - Unit test

Unit test는 function 하나를 검증하기 위해 Zephyr OS 전체를 포함하지 않고 해당 module만 compile해 직접 호출합니다. Build 대상이 작아져 test가 빨라집니다.

tests/unit/에 예제가 있습니다. Source folder의 unit test를 선언하려면 CMake unittest component를 load하고 관련 source를 testbinary target에 추가합니다.

cmake_minimum_required(VERSION 3.20.0)

project(app)
find_package(Zephyr COMPONENTS unittest REQUIRED HINTS $ENV{ZEPHYR_BASE})
target_sources(testbinary PRIVATE main.c)

Kernel의 기본 data structure를 포함하지 않으므로 대상 code가 의존하는 function stub을 test에서 제공해야 합니다. Mock object는 복잡한 실제 object의 동작을 흉내 내고 interaction 발생 여부와 필요하면 호출 순서를 검사해 pass/fail을 정합니다.

Test suite 선언 모범 사례

Twister와 validation tool은 Ztest image가 제공하는 testcase 목록을 source에서 얻을 수 있어야 합니다.

CPP pre-scan 또는 ELF post-scan만 사용하지 않는 이유는 C preprocessing이나 build 자체가 실패하면 하위 testcase를 알아낼 수 없기 때문입니다.

YAML에 testcase를 따로 선언하지 않는 이유는 source와 별도 설명 file을 함께 유지해야 해 중복이 생기기 때문입니다. Source 안에 정보를 두면 변경 때 한 file만 갱신하면 됩니다.

Stress test framework

Zephyr stress test framework(Ztress)는 서로 다른 priority context에서 사용자 function을 실행해 code가 preemption에 견디는지 검증합니다. Context마다 실행 횟수와 preemption 횟수를 추적하며 timeout, 실행 횟수, preemption 횟수를 완료 조건으로 사용할 수 있습니다.

요청한 수의 thread를 서로 다른 priority로 만들고 optional timer를 시작합니다. 각 context가 고유 user function을 호출한 뒤 무작위 system tick 동안 sleep합니다. Framework는 CPU load를 추적하고 sleep 주기를 조정해 load를 높입니다.

Preemption 가능성을 높이려면 system clock frequency가 충분히 높아야 합니다. QEMU x86 기본 100Hz는 너무 낮으므로 100kHz를 권장합니다.

ZTRESS_EXECUTE는 가변 수의 ZTRESS_TIMER·ZTRESS_THREAD context를 priority 내림차순으로 받습니다. 각 context가 최소 실행 횟수와 preemption 횟수를 지정하고 모든 조건을 채우면 report를 출력하고 반환합니다. 실행 중에도 주기적으로 progress report를 냅니다.

ztress_set_timeout() 또는 ztress_abort()로 조기 종료할 수 있습니다. User function은 실행 counter와 마지막 실행 여부 flag를 받습니다.

Timer interrupt context 하나와 thread 두 개를 10초 동안 실행하고, 각 10,000회 실행과 lowest-priority context 1,000회 preemption을 요구하는 예:

ztress_set_timeout(K_MSEC(10000));
ZTRESS_EXECUTE(ZTRESS_TIMER(foo_0, user_data_0, 10000, Z_TIMEOUT_TICKS(20)),
               ZTRESS_THREAD(foo_1, user_data_1, 10000, 0, Z_TIMEOUT_TICKS(20)),
               ZTRESS_THREAD(foo_2, user_data_2, 10000, 1000, Z_TIMEOUT_TICKS(20)));

Configuration

API reference

Test 실행

공식 ztest_test Doxygen group은 suite·test 선언, lifecycle callback, result·phase type과 runner를 생성합니다.

  • ZTEST_SUITE, ZTEST, ZTEST_USER, ZTEST_F, ZTEST_USER_F: suite와 testcase 선언
  • ZTEST_EXPECT_FAIL, ZTEST_EXPECT_SKIP: 예상 result 등록
  • ZTEST_DMEM, ZTEST_BMEM, ZTEST_SECTION: test memory·link section 선언
  • ZTEST_RULE: 모든 testcase에 공통 before/after rule 등록
  • ztest_run_all, ztest_run_test_suites, ztest_run_test_suite: suite 실행
  • ztest_verify_all_test_suites_ran: 모든 suite 실행 여부 검증
  • ztest_test_fail, ztest_test_pass, ztest_test_skip: 현재 testcase result 설정
  • ztest_expected_result, ztest_result, ztest_phase: expected result, 실제 result와 setup/before/test/after/teardown/framework phase 표현

Expected fail API 사용 예:

ZTEST_EXPECT_FAIL(my_suite, test_x);
ZTEST(my_suite, text_x) {
  zassert_true(false, NULL);
}

Expected skip API 사용 예:

ZTEST_EXPECT_SKIP(my_suite, test_x);
ZTEST(my_suite, text_x) {
  zassume_true(false, NULL);
}

Assertion

Assertion이 실패하면 test를 즉시 fail 처리하고 현재 file, line, function, 실패 이유와 optional message를 출력합니다. CONFIG_ZTEST_ASSERT_VERBOSE가 0이면 binary size를 줄이기 위해 file과 line만 출력합니다.

zassert_equal(buf->ref, 2, "Invalid refcount") 실패 출력:

Assertion failed at main.c:62: test_get_single_buffer: Invalid refcount (buf->ref not equal to 2)
Aborted at unit test function

공식 ztest_assert group의 macro:

  • zassert, zassert_unreachable, zassert_true, zassert_false
  • zassert_ok, zassert_not_ok, zassert_is_null, zassert_not_null
  • zassert_equal, zassert_not_equal, zassert_equal_ptr
  • zassert_within, zassert_between_inclusive
  • zassert_mem_equal, zassert_str_equal

Expectation

Expectation이 실패해도 test 실행을 계속하고 마지막에 test를 fail 처리합니다. File, line, function, 이유와 optional message를 출력하며 verbose가 0이면 file과 line만 출력합니다.

두 expectation이 실패하는 예:

zexpect_equal(buf->ref, 2, "Invalid refcount");
zexpect_equal(buf->ref, 1337, "Invalid refcount");

출력은 두 실패를 모두 남긴 뒤 testcase를 fail 처리합니다.

START - test_get_single_buffer
    Expectation failed at main.c:62: test_get_single_buffer: Invalid refcount (buf->ref not equal to 2)
    Expectation failed at main.c:63: test_get_single_buffer: Invalid refcount (buf->ref not equal to 1337)
 FAIL - test_get_single_buffer in 0.0 seconds

공식 ztest_expect group은 true/false, ok/not_ok, null/not_null, equal/not_equal/equal_ptr, within/between_inclusive, mem_equal, str_equal variant를 제공합니다. 대표 macro는 zexpect_true, zexpect_equal, zexpect_mem_equal, zexpect_str_equal입니다.

Assumption

Assumption이 실패하면 test 또는 suite를 즉시 skip합니다. File, line, function, 이유와 optional message를 출력하며 verbose가 0이면 file과 line만 출력합니다.

zassume_equal(buf->ref, 2, "Invalid refcount")이 실패하면 START - test_get_single_buffer, assumption failure 위치와 이유, SKIP - test_get_single_buffer를 출력합니다.

공식 ztest_assume group은 true/false, ok/not_ok, null/not_null, equal/not_equal/equal_ptr, within/between_inclusive, mem_equal, str_equal variant를 제공합니다. 대표 macro는 zassume_true, zassume_equal, zassume_mem_equal, zassume_str_equal입니다.

Ztress

공식 ztest_ztress Doxygen group은 다음 API를 생성합니다.

  • ZTRESS_TIMER, ZTRESS_THREAD, ZTRESS_CONTEXT_INITIALIZER, ZTRESS_EXECUTE: context 선언·실행
  • ztress_handler, ztress_context_data: handler와 context data type
  • ztress_execute, ztress_abort, ztress_set_timeout, ztress_report: 실행·중단·timeout·report
  • ztress_exec_count, ztress_preempt_count, ztress_optimized_ticks: context metric 조회

FFF를 통한 mocking

Zephyr는 FFF(Fake Function Framework)를 통합했습니다. Header를 include합니다.

#include <zephyr/fff.h>

Zephyr는 stub 또는 mock으로 쓸 FFF 기반 fake driver를 제공합니다. Driver instance는 devicetree와 Kconfig로 설정하며 zephyr,fake-can과 zephyr,fake-eeprom binding을 참고할 수 있습니다. 더 간단한 fake function 선언은 아래 FFF extension을 사용합니다.

Test output 사용자 지정

CONFIG_ZTEST_TC_UTIL_USER_OVERRIDE를 y로 설정하고 override를 담은 tc_util_user_override.h를 추가합니다. Project CMakeLists.txt에 zephyr_include_directories(my_folder)를 넣어 header path를 노출합니다.

subsys/testsuite/include/zephyr/tc_util.h에서 override 가능한 macro·define을 확인할 수 있습니다. Override point는 다음 형태의 guard로 둘러싸여 있습니다.

#ifndef SOMETHING
#define SOMETHING <default implementation>
#endif /* SOMETHING */

Test sequence shuffle

기본값에서는 testcase를 alphanumeric 순서로 실행하므로 숨은 순서 dependency가 생길 수 있습니다. CONFIG_ZTEST_SHUFFLE을 활성화하면 순서를 무작위화하고 실패 output에 seed를 표시합니다. Native simulator build는 Twister --seed argument로 seed를 지정할 수 있습니다.

Test 선택

Native simulator용 test binary는 command-line argument로 test를 표시하거나 선택할 수 있습니다. -test는 comma-separated suite::test 목록을 받고 test 이름 대신 *를 쓰면 suite 전체를 실행합니다.

$ zephyr.exe -list
$ zephyr.exe -test="fixture_tests::test_fixture_pointer,framework_tests::test_assert_mem_equal"
$ zephyr.exe -test="framework_tests::*"

FFF extension

공식 fff_extensions group은 fake 설정과 사용을 단순화합니다. RETURN_HANDLED_CONTEXT(FUNCNAME, CONTEXTTYPE, RESULTFIELD, CONTEXTPTRNAME, HANDLERBODY)는 custom fake body를 감싸 호출별 context struct를 꺼냅니다.

Argument는 fake function 이름, custom context type, return type/value field, context pointer 이름, inline handler logic입니다. FFF 내부 구현을 알지 않아도 호출별 state를 가진 fake를 만들 수 있습니다.

struct FUNCNAME##_custom_fake_context
{
    struct instance * const instance;
    int result;
};

int FUNCNAME##_custom_fake(
    const struct instance **instance_out)
{
    RETURN_HANDLED_CONTEXT(
        FUNCNAME,
        struct FUNCNAME##_custom_fake_context,
        result,
        context,
        {
            if (context != NULL)
            {
                if (context->result == 0)
                {
                    if (instance_out != NULL)
                    {
                        *instance_out = context->instance;
                    }
                }
                return context->result;
            }
            return FUNCNAME##_fake.return_val;
        }
    );
}

Source

출처

영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.