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

Zephyr 3.7.0 · Develop · Testing · Deprecated ZTest

폐기 예정 ZTest API

기존 ZTest를 읽고 이식할 수 있도록 구형 suite registry와 mock 기대값·반환값 흐름을 설명합니다.

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

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

Part 1

요약·해설

폐기 예정 ZTest API의 unit test 구성, suite 직접 선언과 registry 방식, 상태 predicate, Twister subcase 탐색 규칙, 구형 mocking framework의 입력·data·반환값 API를 설명합니다.

Suite

declare · register · run

Traceability

Twister · subcase · report

Mock

expect · check · return

Part 2

접을 수 있는 영어 원문 전체

영어 원문 전체 펼치기
원문 SHA-256 44BEAEEB93A9A9B48C586C9E68685AC077CFAA81AFBD66CEA186C98C9D9183F7
.. _test-framework-deprecated:

ZTest Deprecated APIs
#####################

Ztest is currently being migrated to a new API, this documentation provides information about the
deprecated APIs which will eventually be removed. See :ref:`Test Framework <test-framework>` for the new API.
Similarly, ZTest's mocking framework is also deprecated (see :ref:`Mocking via FFF <mocking-fff>`).

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.

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.

.. _main_c_bp:

Best practices for declaring the test suite
===========================================

*twister* and other validation tools need to obtain the list of
subcases 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 project.  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 sub-testcase
   as passed, failed, blocked, or skipped.  Reporting on only the
   high-level test project level, particularly when tests do too
   many things, is too vague.

There exist two alternatives to writing tests. The first, and more verbose,
approach is to directly declare and run the test suites.
Here is a generic template for a test showing the expected use of
:c:func:`ztest_test_suite`:

.. code-block:: C

   #include <zephyr/ztest.h>

   extern void test_sometest1(void);
   extern void test_sometest2(void);
   #ifndef CONFIG_WHATEVER		/* Conditionally skip test_sometest3 */
   void test_sometest3(void)
   {
   	ztest_test_skip();
   }
   #else
   extern void test_sometest3(void);
   #endif
   extern void test_sometest4(void);
   ...

   void test_main(void)
   {
   	ztest_test_suite(common,
                            ztest_unit_test(test_sometest1),
                            ztest_unit_test(test_sometest2),
                            ztest_unit_test(test_sometest3),
                            ztest_unit_test(test_sometest4)
                   );
   	ztest_run_test_suite(common);
   }

Alternatively, it is possible to split tests across multiple files using
:c:func:`ztest_register_test_suite` which bypasses the need for ``extern``:

.. code-block:: C

  #include <zephyr/ztest.h>

  void test_sometest1(void) {
  	zassert_true(1, "true");
  }

  ztest_register_test_suite(common, NULL,
  			    ztest_unit_test(test_sometest1)
  			    );

The above sample simple registers the test suite and uses a ``NULL`` pragma
function (more on that later). It is important to note that the test suite isn't
directly run in this file. Instead two alternatives exist for running the suite.
First, if to do nothing. A default ``test_main`` function is provided by
ztest. This is the preferred approach if the test doesn't involve a state and
doesn't require use of the pragma.

In cases of an integration test it is possible that some general state needs to
be set between test suites. This can be thought of as a state diagram in which
``test_main`` simply goes through various actions that modify the board's
state and different test suites need to run. This is achieved in the following:

.. code-block:: C

  #include <zephyr/ztest.h>

  struct state {
  	bool is_hibernating;
  	bool is_usb_connected;
  }

  static bool pragma_always(const void *state)
  {
  	return true;
  }

  static bool pragma_not_hibernating_not_connected(const void *s)
  {
  	struct state *state = s;
  	return !state->is_hibernating && !state->is_usb_connected;
  }

  static bool pragma_usb_connected(const void *s)
  {
  	return ((struct state *)s)->is_usb_connected;
  }

  ztest_register_test_suite(baseline, pragma_always,
  			    ztest_unit_test(test_case0));
  ztest_register_test_suite(before_usb, pragma_not_hibernating_not_connected,
  			    ztest_unit_test(test_case1),
  			    ztest_unit_test(test_case2));
  ztest_register_test_suite(with_usb, pragma_usb_connected,,
  			    ztest_unit_test(test_case3),
  			    ztest_unit_test(test_case4));

  void test_main(void)
  {
  	struct state state;

	/* Should run `baseline` test suite only. */
	ztest_run_registered_test_suites(&state);

  	/* Simulate power on and update state. */
  	emulate_power_on();
  	/* Should run `baseline` and `before_usb` test suites. */
  	ztest_run_registered_test_suites(&state);

  	/* Simulate plugging in a USB device. */
  	emulate_plugging_in_usb();
  	/* Should run `baseline` and `with_usb` test suites. */
  	ztest_run_registered_test_suites(&state);

  	/* Verify that all the registered test suites actually ran. */
  	ztest_verify_all_registered_test_suites_ran();
  }

For *twister* to parse source files and create a list of subcases,
the declarations of :c:func:`ztest_test_suite` and
:c:func:`ztest_register_test_suite` must follow a few rules:

- one declaration per line

- conditional execution by using :c:func:`ztest_test_skip`

What to avoid:

- packing multiple testcases in one source file

  .. code-block:: C

     void test_main(void)
     {
     #ifdef TEST_feature1
             ztest_test_suite(feature1,
                              ztest_unit_test(test_1a),
                              ztest_unit_test(test_1b),
                              ztest_unit_test(test_1c)
                              );
             ztest_run_test_suite(feature1);
     #endif

     #ifdef TEST_feature2
             ztest_test_suite(feature2,
                              ztest_unit_test(test_2a),
                              ztest_unit_test(test_2b)
                              );
             ztest_run_test_suite(feature2);
     #endif
     }


- Do not use ``#if``

  .. code-block:: C

             ztest_test_suite(common,
                              ztest_unit_test(test_sometest1),
                              ztest_unit_test(test_sometest2),
     #ifdef CONFIG_WHATEVER
                              ztest_unit_test(test_sometest3),
     #endif
                              ztest_unit_test(test_sometest4),
             ...

- Do not add comments on lines with a call to :c:func:`ztest_unit_test`:

  .. code-block:: C

             ztest_test_suite(common,
                              ztest_unit_test(test_sometest1),
                              ztest_unit_test(test_sometest2) /* will fail */,
             /* will fail! */ ztest_unit_test(test_sometest3),
                              ztest_unit_test(test_sometest4),
             ...

- Do not define multiple definitions of unit / user unit test case per
  line


  .. code-block:: C

             ztest_test_suite(common,
                              ztest_unit_test(test_sometest1), ztest_unit_test(test_sometest2),
                              ztest_unit_test(test_sometest3),
                              ztest_unit_test(test_sometest4),
             ...


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 testcase description?

  A separate testcase 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.

Mocking
*******

These functions allow abstracting callbacks and related functions and
controlling them from specific tests. You can enable the mocking framework by
setting :kconfig:option:`CONFIG_ZTEST_MOCKING` to "y" in the configuration file of the
test.  The amount of concurrent return values and expected parameters is
limited by :kconfig:option:`CONFIG_ZTEST_PARAMETER_COUNT`.

Here is an example for configuring the function ``expect_two_parameters`` to
expect the values ``a=2`` and ``b=3``, and telling ``returns_int`` to return
``5``:

.. literalinclude:: ../../../../develop/test/mocking.c
   :language: c
   :linenos:

.. doxygengroup:: ztest_mock

Part 3

한국어 전문 번역

폐기 예정 ZTest API

Ztest는 새 API로 이전 중이며, 이 문서는 앞으로 제거될 구형 API를 설명합니다. 새 코드는 Test Framework의 현재 API를 사용해야 합니다. ZTest 자체 mocking framework도 폐기 예정이므로 새 mocking 코드는 같은 문서의 FFF 방식을 사용합니다.

빠른 시작: Unit test

Ztest는 unit test에 사용할 수 있습니다. 함수 하나를 검사하려고 Zephyr OS 전체를 포함하는 대신, 검증할 module만 compile하고 대상 함수를 직접 호출합니다. Build 범위가 작으므로 test가 빨라지고 실패 원인도 해당 module에 집중됩니다.

다만 대부분의 code가 의존하는 kernel 기본 자료구조를 포함하지 않으므로, test가 호출할 외부 함수의 stub을 직접 제공해야 합니다. 구형 Ztest는 이 stub의 입력과 반환값을 통제하는 간단한 mocking helper를 제공합니다.

Unit test의 mock object는 복잡한 실제 object의 행동을 흉내 냅니다. Test는 특정 상호작용이 발생했는지, 필요하다면 그 순서까지 확인하여 pass 또는 fail을 결정합니다.

Test suite 선언 모범 사례

twister와 다른 validation tool은 Zephyr ztest image가 제공할 subcase 목록을 source에서 얻어야 합니다.

구형 API로 test를 작성하는 방법은 두 가지입니다. 첫 번째는 suite를 직접 선언한 다음 직접 실행하는 자세한 방식입니다. 다음은 ztest_test_suite의 일반적인 사용 template입니다.

#include <zephyr/ztest.h>

extern void test_sometest1(void);
extern void test_sometest2(void);
#ifndef CONFIG_WHATEVER              /* Conditionally skip test_sometest3 */
void test_sometest3(void)
{
     ztest_test_skip();
}
#else
extern void test_sometest3(void);
#endif
extern void test_sometest4(void);
...

void test_main(void)
{
     ztest_test_suite(common,
                         ztest_unit_test(test_sometest1),
                         ztest_unit_test(test_sometest2),
                         ztest_unit_test(test_sometest3),
                         ztest_unit_test(test_sometest4)
                );
     ztest_run_test_suite(common);
}

ztest_test_suite(common, ...)은 common suite에 각 ztest_unit_test(...)를 등록합니다. 조건상 실행할 수 없는 test는 test function 자체에서 ztest_test_skip()을 호출하게 만들고, 마지막에 ztest_run_test_suite(common)으로 suite를 실행합니다.

여러 source file에서 suite 등록

두 번째 방법은 ztest_register_test_suite를 사용하여 test를 여러 source file로 나누는 것입니다. Suite를 전역 registry에 등록하므로 다른 file의 test function을 extern으로 모아 선언하지 않아도 됩니다.

#include <zephyr/ztest.h>

void test_sometest1(void) {
      zassert_true(1, "true");
}

ztest_register_test_suite(common, NULL,
                          ztest_unit_test(test_sometest1)
                          );

이 예제는 common suite와 한 test를 등록하고 predicate 함수 자리에 NULL을 전달합니다. 이 file에서는 suite를 직접 실행하지 않습니다. State와 predicate가 필요 없는 보통 test라면 별도 동작을 하지 않아도 Ztest가 제공하는 기본 test_main이 등록된 suite를 실행하며, 이것이 권장되는 방식입니다.

Integration test에서는 suite 사이에 board의 공통 상태를 바꿔야 할 수 있습니다. 이 경우 test_main은 상태 전이 순서를 수행하고, 각 suite의 predicate가 현재 상태에서 그 suite를 실행할지 결정합니다.

#include <zephyr/ztest.h>

struct state {
      bool is_hibernating;
      bool is_usb_connected;
}

static bool pragma_always(const void *state)
{
      return true;
}

static bool pragma_not_hibernating_not_connected(const void *s)
{
      struct state *state = s;
      return !state->is_hibernating && !state->is_usb_connected;
}

static bool pragma_usb_connected(const void *s)
{
      return ((struct state *)s)->is_usb_connected;
}

ztest_register_test_suite(baseline, pragma_always,
                          ztest_unit_test(test_case0));
ztest_register_test_suite(before_usb, pragma_not_hibernating_not_connected,
                          ztest_unit_test(test_case1),
                          ztest_unit_test(test_case2));
ztest_register_test_suite(with_usb, pragma_usb_connected,,
                          ztest_unit_test(test_case3),
                          ztest_unit_test(test_case4));

void test_main(void)
{
      struct state state;

      /* Should run `baseline` test suite only. */
      ztest_run_registered_test_suites(&state);

      /* Simulate power on and update state. */
      emulate_power_on();
      /* Should run `baseline` and `before_usb` test suites. */
      ztest_run_registered_test_suites(&state);

      /* Simulate plugging in a USB device. */
      emulate_plugging_in_usb();
      /* Should run `baseline` and `with_usb` test suites. */
      ztest_run_registered_test_suites(&state);

      /* Verify that all the registered test suites actually ran. */
      ztest_verify_all_registered_test_suites_ran();
}

예제의 실행 순서는 다음과 같습니다.

  • pragma_always인 baseline은 호출할 때마다 실행됩니다.
  • 초기 상태에서는 ztest_run_registered_test_suites(&state)가 baseline만 실행합니다.
  • 전원을 켠 상태로 바꾸면 baseline과 before_usb가 실행됩니다.
  • USB 연결 상태로 바꾸면 baseline과 with_usb가 실행됩니다.
  • 마지막 ztest_verify_all_registered_test_suites_ran()은 등록된 suite가 적어도 한 번씩 실제 실행되었는지 검증합니다.

Twister가 subcase를 찾을 수 있는 선언 규칙

twister가 source file을 parse하여 subcase 목록을 만들 수 있도록 ztest_test_suite와 ztest_register_test_suite 선언은 다음 규칙을 따라야 합니다.

  • 한 줄에는 하나의 선언만 둡니다.
  • 조건부 실행은 선언 구조를 전처리기로 바꾸지 말고 ztest_test_skip을 사용합니다.

다음 형태는 피해야 합니다.

피해야 할 선언 형태

한 source file 안에서 전처리 조건에 따라 서로 다른 testcase 묶음을 조립하면 정적 parser가 실제 subcase 목록을 안정적으로 얻기 어렵습니다.

void test_main(void)
{
#ifdef TEST_feature1
        ztest_test_suite(feature1,
                         ztest_unit_test(test_1a),
                         ztest_unit_test(test_1b),
                         ztest_unit_test(test_1c)
                         );
        ztest_run_test_suite(feature1);
#endif

#ifdef TEST_feature2
        ztest_test_suite(feature2,
                         ztest_unit_test(test_2a),
                         ztest_unit_test(test_2b)
                         );
        ztest_run_test_suite(feature2);
#endif
}

ztest_test_suite argument 목록 안에 #if 또는 #ifdef를 넣지 않습니다.

        ztest_test_suite(common,
                         ztest_unit_test(test_sometest1),
                         ztest_unit_test(test_sometest2),
#ifdef CONFIG_WHATEVER
                         ztest_unit_test(test_sometest3),
#endif
                         ztest_unit_test(test_sometest4),
        ...

ztest_unit_test 호출과 같은 줄에 comment를 붙이거나, comment 뒤에 호출을 이어 쓰지 않습니다.

ztest_test_suite(common,
                 ztest_unit_test(test_sometest1),
                 ztest_unit_test(test_sometest2) /* will fail */,
/* will fail! */ ztest_unit_test(test_sometest3),
                 ztest_unit_test(test_sometest4),
...

한 줄에 unit test 또는 user unit test 정의를 둘 이상 넣지 않습니다.

ztest_test_suite(common,
                 ztest_unit_test(test_sometest1), ztest_unit_test(test_sometest2),
                 ztest_unit_test(test_sometest3),
                 ztest_unit_test(test_sometest4),
...

다른 탐색 방식을 쓰지 않는 이유

CPP로 먼저 전처리한 뒤 parse하거나 build된 ELF를 뒤에서 scan할 수도 있지만, C preprocessing 또는 build가 어떤 이유로든 실패하면 subcase 목록 자체를 알 수 없습니다. Source를 직접 분석하면 build 실패 상황에서도 계획된 subcase를 report할 수 있습니다.

testcase.yaml에 subcase를 따로 선언하지 않는 이유는 중복 유지보수를 피하기 위해서입니다. Test source와 별도 YAML 목록을 함께 고치게 만들면 둘이 어긋날 수 있지만, source 한 곳만 갱신하면 선언과 구현이 함께 유지됩니다.

Mocking

구형 mocking 함수는 callback과 관련 함수를 추상화하고 test별로 입력과 반환값을 통제합니다. Test configuration에서 CONFIG_ZTEST_MOCKING을 y로 설정해야 합니다. 동시에 저장할 수 있는 반환값과 예상 parameter의 수는 CONFIG_ZTEST_PARAMETER_COUNT가 제한합니다.

다음 예제는 expect_two_parameters가 a=2와 b=3을 받을 것으로 설정하고, returns_int가 5를 반환하도록 설정합니다.

#include <zephyr/ztest.h>

static void expect_two_parameters(int a, int b)
{
	ztest_check_expected_value(a);
	ztest_check_expected_value(b);
}

static void parameter_tests(void)
{
	ztest_expect_value(expect_two_parameters, a, 2);
	ztest_expect_value(expect_two_parameters, b, 3);
	expect_two_parameters(2, 3);
}

static int returns_int(void)
{
	return ztest_get_return_value();
}

static void return_value_tests(void)
{
	ztest_returns_value(returns_int, 5);
	zassert_equal(returns_int(), 5, NULL);
}

void test_main(void)
{
	ztest_test_suite(mock_framework_tests,
		ztest_unit_test(parameter_test),
		ztest_unit_test(return_value_test)
	);

	ztest_run_test_suite(mock_framework_tests);
}

ztest_expect_value가 기대값을 queue에 넣고 mock function 안의 ztest_check_expected_value가 실제 argument를 소비하며 비교합니다. 반환값도 같은 방식으로 ztest_returns_value가 준비하고 ztest_get_return_value가 mock function 안에서 꺼냅니다.

구형 Mock API reference

공식 ztest_mock group은 unit test용 단순 mocking API를 제공합니다. 모두 CONFIG_ZTEST_MOCKING=y가 필요합니다.

  • ztest_expect_value(func, param, value): func의 param에 올 scalar 기대값을 등록합니다. 값은 내부적으로 uintptr_t로 저장됩니다.
  • ztest_check_expected_value(param): 현재 함수에 등록된 기대값이 있는지 확인하고 실제 parameter와 비교합니다. 어느 검사든 실패하면 현재 test를 fail 처리하며, 호출받은 mock 함수 내부에서 사용해야 합니다.
  • ztest_expect_data(func, param, data): parameter가 가리켜야 할 data pointer를 등록합니다. Data 자체가 아니라 pointer만 저장하므로 검사할 때까지 대상 memory가 유효해야 합니다.
  • ztest_check_expected_data(param, length): null 여부와 지정 길이의 data 내용을 기대값과 비교합니다. 호출받은 함수 내부에서 사용합니다.
  • ztest_return_data(func, param, data): func가 param을 통해 돌려줄 data pointer를 등록합니다. 역시 복사 시점까지 data가 유효해야 합니다.
  • ztest_copy_return_data(param, length): 등록한 반환 data를 param이 가리키는 memory로 복사합니다. Null pointer를 먼저 검사하며 호출받은 함수 안에서 사용합니다.
  • ztest_returns_value(func, value): func가 반환할 scalar 값을 등록합니다.
  • ztest_get_return_value()와 ztest_get_return_value_ptr()는 현재 함수의 등록된 반환값을 scalar 또는 void *로 가져옵니다. 값이 미리 등록되지 않았으면 현재 test를 fail 처리합니다.

Source

출처

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