Cmockeryのマニュアル
Cmockery 単体テストフレームワーク
Cmockery はCの単体テストに使用するライブラリである。
目次
ちまたには様々なC言語単体テストフレームワークが存在するが、それらの多くは、複雑であったり、最新のコンパイラ技術を必要としたりする。
そういった単体テストフレームワークを使うのが困難な、古いコンパイラを使うしかない開発というのも、また存在する。
さらに、単体テストフレームワークの多くはテストするコードが、テストを実行しようとしているのと同じプラットホームを対象にしていることを仮定している。この仮定のために、多くのフレームワークはテストするコードモジュール中に標準のCライブラリヘッダをインクルードする必要があるが、それはテスト中のコードが利用する独自または不完全な実装のCライブラリと衝突するかもしれん。
Cmockeryの場合は、テストアプリケーションが、標準Cライブラリヘッダとの衝突が最小限である標準Cライブラリをリンクするだけでよい。また、CmockeryはCコンパイラの新仕様の使用を避けようとする。
結果的に、Cmockeryはさまざまな変なコードをテストできる比較的小さなライブラリになった。しかし、開発者が最新のコンパイラを使ってアプリケーションをテストしたいというだけならば、他の単体テストフレームワークの方が適切かもしれない。
Cmockeryテストは、スタンドアロンの実行形式にコンパイルされ、Cmockeryライブラリ、標準Cライブラリ、そしてテスト対象のモジュールとリンクされる。
いかなるテスト対象モジュール中の外部参照シンボルは模擬化 – そのテストで決められた値を返す関数に置換 – されなければならない、そのテスト内部では。
そのコードモジュールの最終実行対象となる環境と、テストに使用される環境がとんでもなく異なっているかもしれんが、それでもその単体テストは有効である、なぜならテストの目標は機能レベルでのコードモジュールのロジックをテストすることであって、必ずしも最終実行環境中での相互作用のすべてをテストすることではないからだ。
いくらか変更を加えないと、テスト対象モジュールをテストアプリケーションに組み込んでコンパイルできないかもしれない。というわけで、Cmockery単体テストアプリケーションをコンパイルするときはプリプロセッサシンボルUNIT_TESTING が定義されなければならない、そうすることで、そのモジュールのコードがテスト用に条件付きコンパイル可能となる。
Cmockery単体テストケース群はvoid function(void **state)という形式の関数群である。Cmockeryテストアプリケーションはunit_test*()マクロを使って、テストケース関数へのポインタを持つテーブルを初期化する。次に、このテーブルはテストを実行するために、run_tests()マクロに渡される。
run_tests()は、各テスト関数の実行に先立って、適切な例外/シグナルハンドラとその他のデータ構造体をセットアップする。
一つの単体テストが完了すると、run_tests()はそのテストが成功したかどうか決定するために様々なチェックを行う。
run_tests()の使い方
#include <cmockery.h>
// 何もせずに成功するテストケース
void null_test_success(void **state) {
}
int main(int argc, char* argv[])
{
const UnitTest tests[] = { unit_test(null_test_success) };
return run_tests(tests);
}
run_tests()によって、テスト関数が実行される前に、例外/シグナルハンドラは、例外発生時に、単にエラーを表示してテスト関数を抜けるハンドラに上書きされる。テスト関数の外側で例外が起こった場合はたとえばCmokery自身の中とか、アプリケーションの実行をアボートし、エラーコードを返す。
run_tests()から起動されたテスト関数中に「失敗」が発生した場合、そのテスト関数はアボートし、テストアプリケーションの実行は、次のテスト関数から再開される。
テストの「失敗」は、詰まるところCmockery関数のfail()経由のシグナルによって発生する。以下のイベントは、結果的にCmockeryライブラリのテスト「失敗」シグナルを発生させる。
- 確認文(Assertions)
- 例外(Exceptions)
- メモリリーク(Memory leaks)
- セットアップ関数とティアダウン関数の不一致
- 模擬関数の返値がない
- 模擬関数の返値が余る
- あるべき引数の期待値がない
- 引数の期待値が余った
標準Cライブラリのassert()マクロのような 実行時アサートマクロはテスト対象モジュール中では、Cmockeryのmock_assert()関数を使うように再定義されなければならない。
通常、mock_assert()はテスト「失敗」のシグナルを発生する。関数がexpect_assert_failure()マクロを使って呼ばれている場合、その関数内のmock_assert()呼び出しはすべて結果的にテスト実行となる。
expect_assert_failure()から呼ばれた関数中にmock_assert()が一回も呼び出されなかった場合は、テスト「失敗」のシグナル発生が起こる。
mock_assert()の使い方
assert_module.c
——————————————————————————-
#include <assert.h>
// 単体テストが有効化されると assert()マクロを mock_assert()で上書きする
#if UNIT_TESTING
extern void mock_assert(const int result, const char* const expression,
const char * const file, const int line);
#undef assert
#define assert(expression) \
mock_assert((int)(expression), #expression, __FILE__, __LINE__);
#endif // UNIT_TESTING
void increment_value(int * const value)
{
assert(value);
(*value) ++;
}
void decrement_value(int * const value)
{
if (value) {
*value --;
}
}
——————————————————————————-
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>
extern void increment_value(int * const value);
/* This test case will fail but the assert is caught by run_tests() and the
* next test is executed. */
/* このテストケースは失敗するが、アサートはrun_tests()で受け取られて
* (アプリ自体が終わるのではなく)、次のテストが実行される
*/
void increment_value_fail(void **state)
{
increment_value(NULL);
}
// このテストは成功する、increment_value()は
// NULLポインタでアサートするから。
void increment_value_assert(void **state)
{
expect_assert_failure(increment_value(NULL));
}
/*
* このテストは失敗する、decrement_value()はNULLポインタに
* よってアサートしないから
*/
void decrement_value_fail(void **state) {
expect_assert_failure(decrement_value(NULL));}
int main(int argc, char *argv[])
{
const UnitTest tests[] = {
unit_test(increment_value_fail),
unit_test(increment_value_assert),
unit_test(decrement_value_fail),
};
return run_tests(tests);
}
Cmokeryは、テストアプリケーションが、標準Cライブラリのアサートマクロに優先して使用するべき一組のアサートマクロ群を提供する。アサートが失敗した場合、Cmockeryアサートマクロはその失敗を標準エラー出力に書き出し、テスト失敗のシグナルを発生する。
C言語の制限のために、一般的な標準C ライブラリのassert()とCmockeryのassert_ture()とassert_false()マクロはアサートを失敗させた式を表示することしかできない。Cmockeryの型指定アサートマクロ assert_{type}_equal()とassert_{type}_not_equal()は、テスト失敗のデータを表示するが、失敗したテストケースのデバッグのために、データの見栄えをよくしている。
assert_{type}_equal() macrosの使い方
assert_macro.c
——————————————————————————-
#include <string.h>
static const char* status_code_strings[] = {
“Address not found”,
“Connection dropped”,
“Connection timed out”,
};
const char* get_status_code_string(const unsigned int status_code) {
return status_code_strings[status_code];
};
unsigned int string_to_status_code(const char* const status_code_string) {
unsigned int i;
for (i = 0; i < sizeof(status_code_string) / sizeof(status_code_string[0]);
i++) {
if (strcmp(status_code_strings[i], status_code_string) == 0) {
return i;
}
}
return ~0U;
}
——————————————————————————-
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>
extern const char* get_status_code_string(const unsigned int status_code);
extern unsigned int string_to_status_code(
const char* const status_code_string);
/* このテストは失敗する、get_status_code_string(0)が返す文字列が
* “Connection timed out”と一致しないから。 */
void get_status_code_string_test(void **state) {
assert_string_equal(get_status_code_string(0), “Address not found”);
assert_string_equal(get_status_code_string(1), “Connection timed out”);
}
// このテストは失敗する、”Connection timed out”のステータスコードが1じゃないから。
void string_to_status_code_test(void **state) {
assert_int_equal(string_to_status_code(“Address not found”), 0);
assert_int_equal(string_to_status_code(“Connection timed out”), 1);
}
int main(int argc, char *argv[]) {
const UnitTest tests[] = {
unit_test(get_status_code_string_test),
unit_test(string_to_status_code_test),
};
return run_tests(tests);
}
メモリリーク、バッファオーバーフロー/アンダーフローをテストするためにCmockeryのテスト対象モジュールはmalloc(), calloc() そして
free() をそれぞれtest_malloc(), test_calloc()そして
test_free() に置き換えなければならない。test_free()によってメモリブロックの割付解除が行われるたびに、メモリ破壊がチェックされ、もし壊れているブロックが見つかるとテスト失敗 のシグナルが発生する。
test_*()メモリ割付関数を使って、確保されたすべてのメモリはCmockeryライブラリによって追跡される。テスト完了時に割付メモリやメモリリークが残っていると報告されて、テスト失敗のシグナルが発生する。
シンプルってことで、Cmockeryは今のところすべてのテストを一つのプロセス中で実行する。すなわち、すべての一つのテストアプリケーション中のテストケース群は、単一のアドレス空間を共有する。これは、ひとつのテストケースのメモリ破壊によって、テストアプリケーションが、実行途中で不本意に終了してしまう場合がありうることを意味する。
Cmockeryのメモリ割付の使い方
allocate_module.c
——————————————————————————-
#include <malloc.h>
#if UNIT_TESTING
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);
#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif // UNIT_TESTING
void leak_memory() {
int * const temporary = (int*)malloc(sizeof(int));
*temporary = 0;
}
void buffer_overflow() {
char * const memory = (char*)malloc(sizeof(int));
memory[sizeof(int)] = ‘!’;
free(memory);
}
void buffer_underflow() {
char * const memory = (char*)malloc(sizeof(int));
memory[-1] = ‘!’;
free(memory);
}
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>
extern void leak_memory();
extern void buffer_overflow();
extern void buffer_underflow();
// leak_memory()が動的割付ブロックをリークするって失敗するテストケース
void leak_memory_test(void **state) {
leak_memory();
}
// buffer_overflow()が割付ブロックを破壊するって失敗するテストケース
void buffer_overflow_test(void **state) {
buffer_overflow();
}
// buffer_underflow()が割付メモリを破壊するって失敗するテストケース
void buffer_underflow_test(void **state) {
buffer_underflow();
}
int main(int argc, char* argv[]) {
const UnitTest tests[] = {
unit_test(leak_memory_test),
unit_test(buffer_overflow_test),
unit_test(buffer_underflow_test),
};
return run_tests(tests);
}
単体テストは、テスト対象の関数やモジュールをあらゆる外部依存からも分離するべきである。この分離は、静的あるいは動的にテスト対象モジュールにリンクされる模擬関数を使用することで実現されうる。
模擬関数は、対象コードが直接外部関数を参照している場合は静的にリンクされなければならない。動的リンクというのは、単に「テスト対象モジュールが、単体テスト中で定義された模擬関数を参照するために使用する関数ポインタテーブル」をセットするだけである。
模擬関数の実装をシンプルにするために、Cmockeryはwill_return()を使って各テストケース中の模擬関数のために返値を記憶しておく機能を提供している。これらの値は、それぞれの模擬関数がmock()をコールすることで返される。
will_return() に値は渡された値は指定されたそれぞれの関数用のキューに追加される。関数からmock() が連続して呼ばれるたびに、返値はキューから除かれる。そのおかげで、模擬関数がmock()を複数回呼び出すことで、返値のみならず、出力パラメータ(変数へのポインタの引数など)に値を返すことも可能になる。さらに、これにより、模擬関数の複数回呼び出しのための返値のセットも可能となる。
will_return()の使い方
typedef struct DatabaseConnection DatabaseConnection;
/* query_stringにSQL問合せを受け取って、resultsにその問合せの結果の
* ポインタ配列をセットする関数。返値は、問合せ結果配列の要素数。
* 返された結果配列は静的に割当てられていてfree()で解放する必要は
* ない。
*/
typedef unsigned int (*QueryDatabase)(
DatabaseConnection* const connection, const char * const query_string,
void *** const results);
// Connection to a database.
struct DatabaseConnection {
const char *url;
unsigned int port;
QueryDatabase query_database; // 直上のtypedefで定義した関数型
};
// Connect to a database.
DatabaseConnection* connect_to_database(const char * const url,
const unsigned int port);
#include <stddef.h>
#include <stdio.h>
#include <database.h>
#ifdef _WIN32
#define snprintf _snprintf
#endif // _WIN32
// 顧客情報をもつデータベースと接続
DatabaseConnection* connect_to_customer_database() {
return connect_to_database(“customers.abcd.org”, 321);
}
/* 顧客の名前でIDを検索し、見つかると0より大きい返値を返す、
* さにあらずんば0を返す。*/
unsigned int get_customer_id_by_name(
DatabaseConnection * const connection,
const char * const customer_name) {
char query_string[256];
int number_of_results;
void **results;
snprintf(query_string, sizeof(query_string),
“SELECT ID FROM CUSTOMERS WHERE NAME = %s”, customer_name);
number_of_results = connection->query_database(connection, query_string,
&results);
if (number_of_results != 1) {
return -1;
}
return (unsigned int)results[0];
}
customer_database_test.c
——————————————————————————-
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>
#include <database.h>
extern DatabaseConnection* connect_to_customer_database();
extern unsigned int get_customer_id_by_name(
DatabaseConnection * const connection, const char * const customer_name);
// Mock query database function.
unsigned int mock_query_database(
DatabaseConnection* const connection, const char * const query_string,
void *** const results) {
*results = (void**)mock();
return (unsigned int)mock();
}
// データベースに接続する関数のモック。
DatabaseConnection* connect_to_database(const char * const database_url,
const unsigned int port) {
return (DatabaseConnection*)mock();
}
void test_connect_to_customer_database(void **state) {
// DA7ABA53は「DATABASE」のギャル文字
will_return(connect_to_database, 0x0DA7ABA53);
assert_int_equal((int)connect_to_customer_database(), 0x0DA7ABA53);
}
/* This test fails as the mock function connect_to_database() will have no
* value to return. */
/* このテストは、モック関数 connect_to_database()が値を返さない
* ので失敗する */
void fail_connect_to_customer_database(void **state) {
assert_true(connect_to_customer_database() ==
(DatabaseConnection*)0x0DA7ABA53);
}
void test_get_customer_id_by_name(void **state) {
DatabaseConnection connection = {
“somedatabase.somewhere.com”, 12345678, mock_query_database
};
// mock_query_database() は唯一のcustomer IDを返す
int customer_ids = 543;
will_return(mock_query_database, &customer_ids);
will_return(mock_query_database, 1);
assert_int_equal(get_customer_id_by_name(&connection, “john doe”), 543);
}
int main(int argc, char* argv[]) {
const UnitTest tests[] = {
unit_test(test_connect_to_customer_database),
unit_test(fail_connect_to_customer_database),
unit_test(test_get_customer_id_by_name),
};
return run_tests(tests);
}
模擬関数の返値を保存することに加えて、Cmockeryはexpect_*()関数を提供することにより、模擬関数の引数の期待される値を保存する機能を提供する。模擬関数の引数はcheck_expected()マクロを使うことによって、逐次妥当性確認が行われる。
引数ごとにexpect_*()マクロの次々に呼び出すと、指定された引数チェックのためにキューに入れられていく。check_expected() は、expect_*()によって追加された値と関数の引数をチェックし、失敗するとテスト失敗のシグナルが発生する。さらに、check_expected()が呼ばれたが、キューに値がなくなってしまっている場合もテスト失敗が発生する。
expect_*()の使い方
#include <database.h>
// 顧客情報をもつデータベースに接続
DatabaseConnection* connect_to_product_database() {
return connect_to_database(“products.abcd.org”, 322);
}
product_database_test.c
——————————————————————————-
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>
#include <database.h>
extern DatabaseConnection* connect_to_product_database();
/* データベースと接続するモック関数
* NOTE: このモック関数は仮想databse.hモジュールを
* 使用するテスト間で共有される。 */
DatabaseConnection* connect_to_database(const char * const url,
const unsigned int port) {
check_expected(url);
check_expected(port);
return (DatabaseConnection*)mock();
}
void test_connect_to_product_database(void **state) {
expect_string(connect_to_database, url, “products.abcd.org”);
expect_value(connect_to_database, port, 322);
will_return(connect_to_database, 0xDA7ABA53);
assert_int_equal(connect_to_product_database(), 0xDA7ABA53);
}
/* このテストはconnect_to_product_database()によってconnect_to_database()に
* 渡されるURLが期待するURLと異なるせいで失敗する */
void test_connect_to_product_database_bad_url(void **state) {
expect_string(connect_to_database, url, “products.abcd.com”);
expect_value(connect_to_database, port, 322);
will_return(connect_to_database, 0xDA7ABA53);
assert_int_equal((int)connect_to_product_database(), 0xDA7ABA53);
}
/* このテストは、模擬connect_to_database()が、このテスト関数では
* 指定されていないport引数の値を探しそうとして失敗する */
void test_connect_to_product_database_missing_parameter(void **state) {
expect_string(connect_to_database, url, “products.abcd.org”);
will_return(connect_to_database, 0xDA7ABA53);
assert_int_equal((int)connect_to_product_database(), 0xDA7ABA53);
}
int main(int argc, char* argv[]) {
const UnitTest tests[] = {
unit_test(test_connect_to_product_database),
unit_test(test_connect_to_product_database_bad_url),
unit_test(test_connect_to_product_database_missing_parameter),
};
return run_tests(tests);
}
Cmockeryでは、それぞれのテストケースごとの複数のセットアップ関数やティアダウン関数を持つことができる。
unit_test_setup()やunit_test_setup_teardown()マクロによる関数セットアップによって、複数のテストケースで共有される共通の初期化処理を記述できる。さらに、unit_test_teardown()やunit_test_setup_teardown()マクロにティアダウン関数を指定することで、失敗したテストケースにおいてさえ、常に実行されるコード記述の手段を提供している。
unit_test_setup_teardown()の使い方
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
typedef struct KeyValue {
unsigned int key;
const char* value;
} KeyValue;
static KeyValue *key_values = NULL;
static unsigned int number_of_key_values = 0;
void set_key_values(KeyValue * const new_key_values,
const unsigned int new_number_of_key_values) {
key_values = new_key_values;
number_of_key_values = new_number_of_key_values;
}
// ふたつのKeyValue構造体のkeyメンバを比較(qsort用)
int key_value_compare_keys(const void *a, const void *b) {
return (int)((KeyValue*)a)->key – (int)((KeyValue*)b)->key;
}
// valueを指定して、key-valueペアを検索する
KeyValue* find_item_by_value(const char * const value) {
unsigned int i;
for (i = 0; i < number_of_key_values; i++) {
if (strcmp(key_values[i].value, value) == 0) {
return &key_values[i];
}
}
return NULL;
}
// key-valueペア配列をkeyでソートする
void sort_items_by_key() {
qsort(key_values, number_of_key_values, sizeof(*key_values),
key_value_compare_keys);
}
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <string.h>
#include <cmockery.h>
/* このテストで使用するファイル数削減のため、
* setup_teardown.cより複製。*/
typedef struct KeyValue {
unsigned int key;
const char* value;
} KeyValue;
void set_key_values(KeyValue * const new_key_values,
const unsigned int new_number_of_key_values);
extern KeyValue* find_item_by_value(const char * const value);
extern void sort_items_by_key();
static KeyValue key_values[] = {
{ 10, “this” },
{ 52, “test” },
{ 20, “a” },
{ 13, “is” },
};
void create_key_values(void **state) {
KeyValue * const items = (KeyValue*)test_malloc(sizeof(key_values));
memcpy(items, key_values, sizeof(key_values));
*state = (void*)items;
set_key_values(items, sizeof(key_values) / sizeof(key_values[0]));
}
void destroy_key_values(void **state) {
test_free(*state);
set_key_values(NULL, 0);
}
void test_find_item_by_value(void **state) {
unsigned int i;
for (i = 0; i < sizeof(key_values) / sizeof(key_values[0]); i++) {
KeyValue * const found = find_item_by_value(key_values[i].value);
assert_true(found);
assert_int_equal(found->key, key_values[i].key);
assert_string_equal(found->value, key_values[i].value);
}
}
void test_sort_items_by_key(void **state) {
unsigned int i;
KeyValue * const kv = *state;
sort_items_by_key();
for (i = 1; i < sizeof(key_values) / sizeof(key_values[0]); i++) {
assert_true(kv[i - 1].key < kv[i].key);
}
}
int main(int argc, char* argv[]) {
const UnitTest tests[] = {
unit_test_setup_teardown(test_find_item_by_value, create_key_values,
destroy_key_values),
unit_test_setup_teardown(test_sort_items_by_key, create_key_values,
destroy_key_values),
};
return run_tests(tests);
}
ちっちゃなコマンドライン電卓
calculator.c アプリケーションと電卓アプリのフルにテストするテストアプリケーションcalculator_test.cを、このドキュメントで述べたCmockeryの機能の例として提供する。
Last modified: Tue Aug 26 09:33:31 PDT 2008
#include <assert.h>
// 単体テストが有効化されると assert()マクロを mock_assert()で上書きする
#if UNIT_TESTING
extern void mock_assert(const int result, const char* const expression,
const char * const file, const int line);
#undef assert
#define assert(expression) \
mock_assert((int)(expression), #expression, __FILE__, __LINE__);
#endif // UNIT_TESTING
void increment_value(int * const value)
{
assert(value);
(*value) ++;
}
void decrement_value(int * const value)
{
if (value) {
*value –;
}
}
</pre>
One Response to “Cmockeryのマニュアル”
Leave a Reply
Plugin by wpburn.com wordpress themes
[...] Cmockeryのマニュアル [...]