Help us understand the problem. What is going on with this article?

cmockaでCのユニットテスト

cmockaはCのユニットテストライブラリです。

https://cmocka.org/

次のような機能があります。
https://api.cmocka.org/

  • assertマクロ
  • モックのサポート
  • シグナルをキャッチして例外処理
  • メモリリークやバッファオーバーフロー、アンダーフローをテストできる
  • いくつかの出力形式をサポート(stdou, XUnit xml, etc.)

インストール

RHEL系OSであればEPELでパッケージが提供されているので、それを使うのがよさそう。

# yum install epel-release
# yum install libcmocka-devel

テストの実行

実行環境はCentOS7, EPELのlibcmocka-develパッケージがインストールされています

基本的なサンプル

assert_test.c
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <cmocka.h>

static void simple_assert_example(void **state) {
        int *test_state = *state;
        assert_int_equal(*test_state, 100);
}

static int setup(void **state) {
        int *test_state = malloc(sizeof(int));
        if (test_state == NULL) {
                return -1;
        }
        *test_state = 100;
        *state = test_state;
        return 0;
}

static int teardown(void **state) {
        free(*state);
        return 0;
}

int main(void) {
        const struct CMUnitTest tests[] = {
                cmocka_unit_test(simple_assert_example),
        };
        return cmocka_run_group_tests(tests, setup, teardown);
}

main関数ではcmocka_unit_testにテスト関数を引数にしてCMUnitTest構造体を作成しています。
テスト関数の型は(void **state)を引数にとり戻り値voidである必要があります。引数の**stateはテスト前後に呼び出される setup, teardown 関数との間で値を受け渡すために使用されます。

cmocka_run_group_testsでテストを実行しています。第一引数はCMUnitTest構造体の配列, 第二引数と第三引数はテストの前後に呼び出される関数を指定します。不要であればNULLを渡すこともできます。

assert_int_equal, assert_string_equal, assert_ptr_equal, assert_nullなどのマクロで値の検証を行うことができます。
https://api.cmocka.org/group__cmocka__asserts.html

コンパイルの際にはライブラリのリンクオプションにcmockaを指定します。

gcc -o assert_test.bin assert_test.c -lcmocka
./test.bin
[==========] Running 1 test(s).
[ RUN      ] simple_assert_example
[       OK ] simple_assert_example
[==========] 1 test(s) run.
[  PASSED  ] 1 test(s).

出力形式を変えたい場合はcmocka_run_group_testsを呼び出す前にcmocka_set_message_outputでフォーマットを変更します。

int main(void) {
        const struct CMUnitTest tests[] = {
                cmocka_unit_test(simple_assert_example),
        };
        // out put format is CM_OUTPUT_STDOUT by default, other options : CM_OUTPUT_SUBUNIT, CM_OUTPUT_TAP or CM_OUTPUT_XML.
        cmocka_set_message_output(CM_OUTPUT_XML);
        return cmocka_run_group_tests(tests, setup, teardown);
}

CM_OUTPUT_XMLを指定すると出力は次のような感じになります。

<?xml version="1.0" encoding="UTF-8" ?>
<testsuites>
  <testsuite name="tests" time="0.000" tests="1" failures="0" errors="0" skipped="0" >
    <testcase name="simple_assert_example" time="0.000" >
    </testcase>
  </testsuite>
</testsuites>

メモリ割り当てのテスト

cmockaではメモリの問題をテストするためのラッパー関数が用意されています。

cmocka.hには次のような定義があります。

#ifdef UNIT_TESTING
#define malloc test_malloc
#define realloc test_realloc
#define calloc test_calloc
#define free test_free
#endif /* UNIT_TESTING */

ヘッダーの読み込み前にUNIT_TESTING定数を定義しておくことで、既定の関数がcmockaのメモリチェック用関数で置き換わり、メモリの割り当てに関する問題を検出してくれるようになります。

#define UNIT_TESTING 1
#include <cmocka.h>

static void test_free1(void **state) {
        int *ptr_int = malloc(sizeof(int));
        free(ptr_int);
}
static void test_free2(void **state) {
        int *ptr_int = malloc(sizeof(int));
        //free(ptr_int);
}

上記サンプルのtest_free2は割り当てたメモリが解放されていないため、テストが失敗します。

# make test
gcc -o test.bin memory_test.c -lcmocka
./test.bin
[==========] Running 2 test(s).
[ RUN      ] test_free1
[       OK ] test_free1
[ RUN      ] test_free2
[  ERROR   ] --- Blocks allocated...
memory_test.c:17: note: block 0xa11080 allocated here
ERROR: test_free2 leaked 1 block(s)

[  FAILED  ] test_free2
[==========] 2 test(s) run.
[  PASSED  ] 1 test(s).
[  FAILED  ] 1 test(s), listed below:
[  FAILED  ] test_free2

 1 FAILED TEST(S)
make: *** [test] Error 1

Mock

cmockaではコードの一部をモック関数に置き換えてテストするためのヘルパー関数が提供されています。

モック自体はリンカの機能を利用して実現します。コンパイル時に gcc のオプションとして "-Wl,--wrap=FUNC_TO_MOCK" を指定することで実際の関数の代わりにモックが呼び出されるようにします。

下記はmanページからの抜粋です。

Man "ld"
  --wrap=symbol
     Use a wrapper function for symbol. Any undefined reference to symbol
     will be resolved to "__wrap_symbol".  Any undefined reference to
     "__real_symbol" will be resolved to symbol.

Man "gcc"
  Linker Options
    (...omit) -Wl,option  -Xlinker option -u symbol

例として、下記のような関数をテストするケースを考えてみます。

int read_from_device_at_position(int val) {
        return device_read(val);
}

device_readは何らかのハードウェアデバイスにアクセスする関数であると仮定します。
read_from_device_at_positionをテストするためにはdevice_readをモックで置き換える必要があります。

モックとテスト関数は次のように定義します。

int __wrap_device_read(int position) {
        check_expected(position);
        return mock();
}
static void simple_mock_example(void **state) {
        int ret;
        will_return(__wrap_device_read, 256);
        expect_value(__wrap_device_read, position, 52);
        ret = read_from_device_at_position(52);
        assert_int_equal(ret, 256);
}

__wrap_device_readはread_from_device_at_positionで呼び出されているdevice_readのモック関数です。このモック関数はコンパイル時のオプションに "-Wl,--wrap=device_read" を含めることで実際の関数の代わりに呼び出されるようになります。
コンパイルコマンドは次のような感じになります。

# gcc -o test.bin test.c mock_test.c -Wl,--wrap=device_read

テスト関数simple_mock_exampleでは引数と戻り値の定義を行っています。
will_returnは__wrap_device_readが返す値を定義しています。この値は関数内でヘルパー関数を介して取り出すことが出来ます。
expect_valueは__wrap_device_readの引数として渡されるべき値を定義しています。ここで定義した値が渡されたことをヘルパー関数を使用して検証します。

モック関数__wrap_device_readではパラメーターの検証と、事前に定義された値の返却を行っています。
check_expectedはテスト関数のwill_returnで定義された通りのパラメーターが渡されているかどうかを検証しています。
mock()は事前に定義されたモック関数の戻り値を取り出してそのまま返しています。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away