C言語のテストフレームワーク unity

2024/09/12

“Unity”というとゲームフレームワークとして有名だが、これはそうではない。
C言語で書かれたテストフレームワークである。

Unity — Throw The Switch

その昔、私はモックというかスタブというかに meekrosoft/fff: A testing micro framework for creating function test doubles を使っていた。 それが GoogleTest を使っていたのでそのまま使っていた。
私も何度か記事を書いている。

といっても覚えてないのだけどね。

今回は ncs ~というか Zephyr~ が使っている Unity Test を使ってみよう(Zephyrは別だった)。


Unity Test

リンクが面倒ではないのがよいです。
Cソースが 1つと Hファイルが 2つ(片方は internal用)。

動作環境は POSIX系であるというような前提がないので、うまくやれば組み込みの実行環境でも動かせるらしい。
例えば文字出力はマクロになっていて、定義することで出力方法を決めることができるようになっている(config)。

#define UNITY_OUTPUT_CHAR(a) (void)putchar(a)

サンプルを作ってみた。
複数のファイルにテストを書きたい場合を想定して example_2 を参考に extras/fixture を使ってみた。

c-unity-test-ex1

setUp()/tearDown()をファイル単位にするため、グループ名をTEST_GROUP()で付け、TEST_SETUP()/TEST_TEAR_DOWN()を使うなどしている。

rubyを使えば便利なことが出来そうなことが書いてあるが、そこは私は遠慮しておこう。

CMock

Unity と同じ開発者がモックを作る CMock というフレームワークも公開している。

CMock — Throw The Switch

ncs でも 「Unity and CMock」 といっているので、セットで使うことを想定している。
外部機器や乱数のようにテストするときに実際に接続できなかったり、意図した値を出力してくれないとテストにならなかったりする場合に、代わりの関数を自作するなどして意図した効果を出せるようにする方法?である。
前述の meekrosoft/fff もそういったツールである。

CMock uses Ruby scripts to auto-generate C source modules conforming to the interfaces specified in your C header files.

CMock はヘッダファイルからモックを自動的に作ってくれるそうだ(rubyがいる)。

Getting Started

example があったので先に動かしてみて考えようと思ったのだが、なんだかよくわからない・・・。
どうも 1回目の make と 2回目の make で違うようで、たぶん 1回目が準備、2回目がモック生成というところなのだと思う。
作られるファイルが多すぎて、これはこれでなんだかわからなかった。 あきらめてドキュメントを読もう。

What Exactly Are We Talking About Here?

たとえば、これをモックにしたいとする。

int DoesSomething(int a, int b);

この関数を Generated Mock Module Summary を見て分類すると retval func(params)タイプになる。
なのでこれの期待した値(Expect)を返す設定関数は void func_ExpectAndReturn(expected_params, retval_to_return) になる。
つまりこうだ。

void DoesSomething_ExpectAndReturn(int a, int b, int toReturn);

しかし、引数はなんでもいいから期待した値を返せば良い(ExpectAnyArgs)という場合もある。
その場合はこうだ。

void DoesSomething_ExpectAnyArgsAndReturn(int toReturn);

引数がないのでtoReturnは固定値かグローバル変数を使うことになるだろう。

という予想で make_example にコードを追加してやってみた。
難しかったので fork して

サンプル

失敗させないとよくわからないので test_main_foo() で失敗させている。 foo()の戻り値はfoo_ExpectAndReturn(1, 2, 3)として3を返すようにしている。 が

void test_main_foo(void)
{
    foo_ExpectAndReturn(1, 2, 3);
    TEST_ASSERT_EQUAL_INT(6, foo_main(1, 2));
}

foo_main()foo()を3倍した値を返すので、6 と比較して NG となるわけである。

int foo_main(int a, int b)
{
    return foo(a, b) * 3;
}

これがその失敗メッセージ。
6を期待したのだが9が来ました、というわけである。

test_main_foo:FAIL: Expected 6 Was 9

foo_ExpectAndReturn()などを実行するたびに、返すべき値は上書きではなくスタックされる。
なので、スタックした分が消費されずにテストが終わると例えばこんなメッセージが出力される。

.\test\test_main.c:20:test_main_foo:FAIL:Function foo.  Called fewer times than expected.

あとは、スタブというかモックというかを呼び出した引数もスタックされ、テスト関数でこの値を与えたらスタブはこの引数で呼ばれるはず、というようなテスト項目を作ることができるはずである。 そうでないと func_ExpectAndReturn()などで引数をもらう意味が無いだろう。。。

などと思っていたが、これはfunc_ExpectAndReturn()でパラメータと戻り値の両方を保存しておき、スタブが呼び出されたときにこの値で引数チェックを行っているようだ。
たとえばこのように、実際に呼び出すのはfoo(1, 2)の予定なのにfoo_ExpectAndReturn(2, 2, 3)とする。

void test_main_foo(void)
{
    foo_ExpectAndReturn(2, 2, 3);
    TEST_ASSERT_EQUAL_INT(9, foo_main(1, 2));
}

そうすると、仮引数a2を期待したのに1が来ました、というメッセージが出力される。

.\test\test_main.c:22:test_main_foo:FAIL: Expected 2 Was 1. Function foo Argument a. Function called with unexpected argument value.

CMock をインストールするときに gitmodules で Unity もダウンロードされる。 なので CMock をインストールすれば Unity はわざわざインストールせずともよさそうだ。

Unityのサンプルに cmock を使うサンプルを無理やり追加した。

cmock

src/main.c から target1_func()target2_func()を呼び出すようにし、それらをモックにするようにした。

ヘッダファイルが同じ名前というのに苦しめられた。
そもそもインクルードガードが同じ定義名になっているのもダメだったのだが、cmock はデフォルトでヘッダファイル名でモックの関数を作るので名前がかぶってリンクエラーになったのだ。
やむなく YAML ファイルで指定して違う名前を吐き出させるようにした。
Makefileも cmock を使うテストと使わないテストに分離するのが面倒だった。 おそらく ruby で何かすればもうちょっと楽だったんじゃないかと思う。 Makefileは汚いが、もう修正する気力もなかったのでそのままだ。