ncsで使うテストフレームワークはどれだ (4)
2024/09/20
前回の続き。
nRF Connect SDK がサポートする Testing with Unity and CMock を試す。
前回まで
- Windows環境では Unity でのテストはできない
- WSL2 ではできる
- WSL2 から Windows 側に置いたコードを
west buildするととても遅いが WSL2 側に置くとそうでもない - ncs で Unity を使う場合、たぶんだが 1回で 1モジュール分のテストしかできない
- モックだけでなく
setUp()などもstaticや別の名前になどができなさそう
- モックだけでなく
nrf/applications/asset_tracker_v2はtests/の下にテストの数だけフォルダを作っているようなので参考にできそう
asset_tracker_v2 のテスト
asset_tracker_v2 のテストを native_posix_64 で動かしてみたが、json_common, lwm2m_codec_helpers, lwm2m_integration はビルド中エラーになった。
それ以外は動いたようなので、そちらを参考にしよう。
ビルドできなかったものも testcase.yaml を見ると extra_configs に CONFIG~=y があるので、何かすれば動きそうな気はする。
ui_moduleテスト
ui_module を見ることにする。
CMakeLists.txt
とりあえず思ったのは、cmake がある程度分かってないとダメだなということ。
vscode で色が付かない test_runner_generate や cmock_handle は Unity の cmake関数だというのはなんとなく分かるが、
set_propertyで何やってるのかなどが頭に入ってこない。
set(ASSET_TRACKER_V2_DIR ../..)- プロジェクトの先頭を自動的に見つけるような機能はないので、自分で設定する
${ZEPHYR_NRF_MODULE_DIR}- たぶん
ncs/vX.Y.Z/nrf/のパスが入っている。 ${ZEPHYR_NRFXLIB_MODULE_DIR}も似たようなものだろう。- どこで設定しているのか分からん。westが設定するのか?
- たぶん
cmock_handle()は4つ分target_sources()はテスト対象コードとテストコードの2つ分target_include_directories()はgcc -I相当だろうtarget_compile_definitions()- “definitions”なのに
-Dは書かないといかんのか?- なくてもよさそう
- gcc系以外だと違うオプションのこともあるだろうから、そうせざるを得ないのか
- “definitions”なのに
set_property()- CONPILE_FLAGS
"-include ~": gccのオプション- 対象のコードの先頭に
#includeを追加するものらしい ui_module.hはテストコードの中では使われない。ここだけ。SYS_INITを#undefして空定義しなおしている- SYS_INIT
- kernel への初期化関数登録なのでテストでは不要だから空定義して消している
- それだったら
staticと同じようにCONFIG_UNITYで#ifdefしてもいいんじゃないのかという気はする。
- 対象のコードの先頭に
そう、テスト対象のモジュールに 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.confはCONFIG_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

うむ。
ちなみに CMock で作ったコードを通った分の*.gcnoなどは build/CMakeFiles/app.dir/mocks/ にある。
今回のまとめ
Unity + CMock で ncs のテストがある程度はできることが分かった。 カバレッジも取れていると思う。
気になるのは DT_ALIAS() のような DeviceTree 関係のコードだ。
今回の asset_tracker_v2 では src/ext_sensors/ext_sensors.c が DT_ALIAS() を使っているのだがそのテストコードはなかった。
テストなのできれいに解決する必要はないので、適当にごまかせればよいか。
example_test の書き換え
今回を参考に example_test を書き直した。
tests/ に移動して、その中に uut と foo のテストをそれぞれ置くようにした。