ncsで使うテストフレームワークはどれだ (4)

2024/09/20

前回の続き。

nRF Connect SDK がサポートする Testing with Unity and CMock を試す。

前回まで

asset_tracker_v2 のテスト

asset_tracker_v2 のテストを native_posix_64 で動かしてみたが、json_common, lwm2m_codec_helpers, lwm2m_integration はビルド中エラーになった。 それ以外は動いたようなので、そちらを参考にしよう。
ビルドできなかったものも testcase.yaml を見ると extra_configsCONFIG~=y があるので、何かすれば動きそうな気はする。

ui_moduleテスト

ui_module を見ることにする。

CMakeLists.txt

とりあえず思ったのは、cmake がある程度分かってないとダメだなということ。
vscode で色が付かない test_runner_generatecmock_handle は Unity の cmake関数だというのはなんとなく分かるが、 set_propertyで何やってるのかなどが頭に入ってこない。

そう、テスト対象のモジュールに static関数を見えるようにするためのコードが既に入っているのだ。

#if defined(CONFIG_UNITY)
#define STATIC
#else
#define STATIC static
#endif

複数モジュールをまとめてテストコードにするならstatic関数名が重なる心配も出てくるが、少なくとも今回は気にしなくてよい。

既にテスト用かどうかでコードが切り替えられるようになってるなら CMakeLists.txt でがんばらなくていいんじゃないのかと思ったのだった。
まあ、大したことではない。 それに#includeのところまで見たわけではないので、こうじゃないとダメなのかもしれない。

vars_internal.h

なんでこれは別ファイルにしたのだろう?

といろいろ見たが、これはテスト対象のモジュール内で定義しているグローバル変数だった。
普段は static でテストの時には参照できるようになるものの、extern している訳でもないからテスト関数で使うとコンパイルエラーになる。 なのでプロトタイプ宣言しておこう、という役割のようだ。

テスト対象のモジュールは 1つ、テストも 1ファイルに収めるならばいっそのことテスト対象のソースファイルを#includeしてしまってもよいかと思う。
fffではそうすることで static の置き換えもせずに済んだ。

まあ、ソースファイルを#includeするのにちょっと抵抗はあるかもしれないが、なに、すぐ慣れる。
実際に ui_module でやってみたが、多少の書き換えでテストが通ったので、そういうやり方でもいけそうだ。

ui_module_test.h をなくすためにSYS_INIT#ifdefで無効化するとsetup()がテストされていないので unused の warning が出る。 テストしてもいいんじゃないかね。

void test_setup(void)
{
	__cmock_dk_buttons_init_ExpectAnyArgsAndReturn(0);
	TEST_ASSERT_EQUAL_INT(0, setup());

	__cmock_dk_buttons_init_ExpectAnyArgsAndReturn(-EIO);
	TEST_ASSERT_EQUAL_INT(-EIO, setup());
}

Kconfig, prj.conf, testcase.yaml

Kconfigはほぼ空っぽ。

prj.confCONFIG_UNITY=y以外は本体のprj.confからいるものだけ持ってきたのかな?

testcase.yamlはたぶん今回は未使用。

ui_module_test.c

あとはテストコードだけなのだが、これはちょっと見るのがつらい。

テスト関数の先頭で resetTest() を呼び出していることが気になった。 テストランナーが展開されたときに自動で作られるのだが、テストコードごとに違うことはおそらくなかろう。

/*=======Test Reset Options=====*/
void resetTest(void);
void resetTest(void)
{
  tearDown();
  CMock_Verify();
  CMock_Destroy();
  CMock_Init();
  setUp();
}
void verifyTest(void);
void verifyTest(void)
{
  CMock_Verify();
}

ランナー本体はこうだった。

static void run_test(UnityTestFunction func, const char* name, UNITY_LINE_TYPE line_num)
{
    Unity.CurrentTestName = name;
    Unity.CurrentTestLineNumber = line_num;
#ifdef UNITY_USE_COMMAND_LINE_ARGS
    if (!UnityTestMatches())
        return;
#endif
    Unity.NumberOfTests++;
    UNITY_CLR_DETAILS();
    UNITY_EXEC_TIME_START();
    CMock_Init();
    if (TEST_PROTECT())
    {
        setUp();
        func();
    }
    if (TEST_PROTECT())
    {
        tearDown();
        CMock_Verify();
    }
    CMock_Destroy();
    UNITY_EXEC_TIME_STOP();
    UnityConcludeTest();
}

TEST_PROTECT()false なら resetTest()を呼び出しておかないと不都合がありそうだが、それだとテストコードも呼び出されないことになる。
Unityの説明によると、無限ループしたりなどして終わらないのを何とかしてくれるらしい。 setjmp/longjmpなどをうまいこと使うようだ。

ということは、このTEST_PROTECT()で囲むのは「そういう書き方」なだけみたい。
ならば、やはりresetTest()は毎回呼び出さなくてもよいのではと思う。 が、これはCMock の方に書いてあった。 同じテスト関数内でresetTest()を呼び出してからテストすればいいよ、ということだろう。
“Call it during a test to have CMock validate everything” っていってるけど、評価するのは CMock なのかね。 まあいいや。

テストの個数はテスト関数(test_)の数のようだ。
グループなどもなさそうなので、1テスト関数で 1つの対象については全部のテストをやってしまった方が良いのか?
いや、テスト結果には個数しか出てこないのでテスト関数名で何のテストをしているかわかるくらいの方がやりやすいか。

カバレッジ

Unit Test といえばカバレッジだろう(個人の感想です)。

実機などで動かすこともできるだろうが、わざわざ WSL2 まで使って動かし native_posix_64 などでネイティブ環境を使っているのだ。 カバレッジを取らなくてはなるまい。

$ sudo apt install lcov
target_compile_options(app PRIVATE
	-fprofile-arcs
	-ftest-coverage
	-ggdb
)

target_link_libraries(app PRIVATE
	-lgcov
)
$ west build -b native_posix_64 -t run -p
...
...
$ lcov --capture --directory build/CMakeFiles/app.dir/src/ --output-file lcov.info
$ genhtml lcov.info --output-directory lcov-output --show-details --legend

image

うむ。
ちなみに CMock で作ったコードを通った分の*.gcnoなどは build/CMakeFiles/app.dir/mocks/ にある。

今回のまとめ

Unity + CMock で ncs のテストがある程度はできることが分かった。 カバレッジも取れていると思う。

気になるのは DT_ALIAS() のような DeviceTree 関係のコードだ。
今回の asset_tracker_v2 では src/ext_sensors/ext_sensors.cDT_ALIAS() を使っているのだがそのテストコードはなかった。 テストなのできれいに解決する必要はないので、適当にごまかせればよいか。

example_test の書き換え

今回を参考に example_test を書き直した。
tests/ に移動して、その中に uutfoo のテストをそれぞれ置くようにした。

example_test