Unity とは?
「Unity ってゲームのやつかな? でも C言語?」とお思いでしょうが、ここでの Unity
はC 言語
のためのテストフレームワーク
のほうです。テストケースを書いて、赤色を緑色にしていくやつですね。
小さくて手軽で書きやすいので、簡単にテストを書いて実行することができます。アサーションも必要なものがそろっています ( のちほどご説明 )。
以下では Unity の導入とテストケースの作成、ビルドと実行についてメモします。
導入
- Unity を入手します: https://github.com/ThrowTheSwitch/Unity
- プロジェクトの
lib
等、好きな場所に置きます - テストケースを書きます
- テストランナーを書きます
- モジュールを空実装します
- ビルドの設定をします
- テストを実行して真っ赤にします (なりませんが……)
- モジュールを修正します
- 緑にします
試す
お試しに適当なモジュールを作って、Unity でテストしてみます。
- 最近 2 の n 乗をよく求めるので 2 の n 乗する関数を作ろう ---( 機能A:
my_math
) - 顔文字であいさつ定型文を出すやつを作ろう ---( 機能B:
my_string
)
これらの機能に対して、次の条件を満たすか適当な範囲をチェックしてみます:
-
機能A:
my_math
-
0
が与えられたときは常に定数1
を返そう - 負の数が与えられたときはわかんないのでとにかく
-1
を返そう -
10
以上はヤバそうなので-100
を返そう
-
-
機能B:
my_string
- あいさつは 3 種類用意
- 0 のときは
"(^O^) < こんにちは"
- 1 のときは
"(T_T) < さようなら"
- それ以外はすべて
"(・v・) < ごきげんよう"
テストケースを作る
テストケースはテストグループを作成し、その中でテストケースの処理を定義してまとめて定義できます。
各テストは TEST_GROUP( group )
でテストケースのグループを作ります。group
はテストグループ名で、グループを指定する際に使用します。
TEST_SETUP( group )
マクロと TEST_TEAR_DOWN( group )
マクロを使用することで、各テストケースの開始と終了時に処理を行うことができます。が、今回は空で置いておきます。
TEST( group, case )
で group
に属したテストケースを定義していきます。
機能A のテストです。ここでは単純な整数値の一致のアサーションを使用します:
#include <unity.h>
#include <unity_fixture.h>
#include "my_math.h"
TEST_GROUP( MyMath );
TEST_SETUP( MyMath )
{}
TEST_TEAR_DOWN( MyMath )
{}
/**
* 0 のときは常に 1 を返します
* 0 を与えて 1 が返ってくることをテストします
*/
TEST( MyMath, my_math_should_return_1_where_exponent_is_0 )
{
TEST_ASSERT_EQUAL( 1, my_math( 0 ) );
}
/**
* 負数では -1 を返します
* 適当な負数をテストしてみます
*/
TEST( MyMath, my_math_should_return_minus_1_where_exponent_is_negative )
{
TEST_ASSERT_EQUAL( -1, my_math( -1 ) );
TEST_ASSERT_EQUAL( -1, my_math( -2 ) );
TEST_ASSERT_EQUAL( -1, my_math( -3 ) );
TEST_ASSERT_EQUAL( -1, my_math( -4 ) );
TEST_ASSERT_EQUAL( -1, my_math( -5 ) );
TEST_ASSERT_EQUAL( -1, my_math( -5928 ) );
}
/**
* 10 以降では -100 を返します
* 適当な大きい数をテストしてみます
*/
TEST( MyMath, my_math_should_return_minus_100_where_exponent_is_larger_than_10 )
{
TEST_ASSERT_EQUAL( 1024, my_math( 10 ) );
TEST_ASSERT_EQUAL( -100, my_math( 11 ) );
TEST_ASSERT_EQUAL( -100, my_math( 15 ) );
TEST_ASSERT_EQUAL( -100, my_math( 400 ) );
TEST_ASSERT_EQUAL( -100, my_math( 59284 ) );
TEST_ASSERT_EQUAL( -100, my_math( 213007 ) );
}
/**
* 1 〜 10 の範囲では 2^n を返します
* すべてテストしてみます
*/
TEST( MyMath, my_math_should_return_powered_2_otherwise )
{
TEST_ASSERT_EQUAL( 2, my_math( 1 ) );
TEST_ASSERT_EQUAL( 4, my_math( 2 ) );
TEST_ASSERT_EQUAL( 8, my_math( 3 ) );
TEST_ASSERT_EQUAL( 16, my_math( 4 ) );
TEST_ASSERT_EQUAL( 32, my_math( 5 ) );
TEST_ASSERT_EQUAL( 64, my_math( 6 ) );
TEST_ASSERT_EQUAL( 128, my_math( 7 ) );
TEST_ASSERT_EQUAL( 256, my_math( 8 ) );
TEST_ASSERT_EQUAL( 512, my_math( 9 ) );
TEST_ASSERT_EQUAL( 1024, my_math( 10 ) );
}
次に機能B のテストです。ここでは文字列の処理なので、文字列のアサーションを使用します:
#include <unity.h>
#include <unity_fixture.h>
#include "my_string.h"
TEST_GROUP( MyString );
/**
* 0 のときは こんにちは の定型文を書き込む
* 予想される文字列であることをテストします
*/
TEST( MyString, my_string_should_return_hello_where_0_is_given )
{
char buffer[256] = {};
my_string( 0, buffer );
TEST_ASSERT_EQUAL_STRING( "(^O^) < こんにちは", buffer );
}
/**
* 1 のときは さようなら の定型文を書き込む
* 予想される文字列であることをテストします
*/
TEST( MyString, my_string_should_return_goodbye_where_1_is_given )
{
char buffer[256] = {};
my_string( 1, buffer );
TEST_ASSERT_EQUAL_STRING( "(T_T) < さようなら", buffer );
}
/**
* それ以外は ごきげんよう の定型文を書き込む
* 予想される文字列であることを適当な範囲でテストします
*/
TEST( MyString, my_string_should_return_greeting_otherwise )
{
char buffer[256] = {};
my_string( 2, buffer );
TEST_ASSERT_EQUAL_STRING( "(・v・) < ごきげんよう", buffer );
my_string( 10, buffer );
TEST_ASSERT_EQUAL_STRING( "(・v・) < ごきげんよう", buffer );
my_string( -50, buffer );
TEST_ASSERT_EQUAL_STRING( "(・v・) < ごきげんよう", buffer );
my_string( 1000, buffer );
TEST_ASSERT_EQUAL_STRING( "(・v・) < ごきげんよう", buffer );
}
テストランナーを作る
テストを作成した後は、手動で各テストケースのテストランナーとすべてのテストケースのランナーを作成する必要があります。自動ではありませんので ものすごい忘れやすい です。
ただし、Unity に添付されている examples/
フォルダー以下の example_1
と example_3
では ruby
で自動でランナーを作成するやり方があるので、それを改変することもできます ( ここでは説明は省きます )。
各テストに関してテストランナーを作ります:
#include <unity.h>
#include <unity_fixture.h>
TEST_GROUP_RUNNER( MyMath )
{
RUN_TEST_CASE( MyMath, my_math_should_return_1_where_exponent_is_0 );
RUN_TEST_CASE( MyMath, my_math_should_return_minus_1_where_exponent_is_negative );
RUN_TEST_CASE( MyMath, my_math_should_return_minus_100_where_exponent_is_larger_than_10 );
RUN_TEST_CASE( MyMath, my_math_should_return_powered_2_otherwise );
}
#include <unity.h>
#include <unity_fixture.h>
TEST_GROUP_RUNNER( MyString )
{
RUN_TEST_CASE( MyString, my_string_should_return_hello_where_0_is_given );
RUN_TEST_CASE( MyString, my_string_should_return_goodbye_where_1_is_given );
RUN_TEST_CASE( MyString, my_string_should_return_greeting_otherwise );
}
すべてのテストケースのランナーを作ります:
#include <unity_fixture.h>
static void RunAllTests(void)
{
RUN_TEST_GROUP( MyMath );
RUN_TEST_GROUP( MyString );
}
int main( int argc, char const **argv )
{
return UnityMain( argc, argv, RunAllTests );
}
構成
今回のプロジェクトはこんな感じのフォルダー構成にします。:
unity_test/
│
├ lib
│ └ unity
│ └ ( もろもろのファイル…… )
│
├ include: モジュール ヘッダーファイル
│ ├ my_math.h
│ └ my_string.h
│
├ src: モジュール ソースファイル
│ ├ my_math.c
│ └ my_string.c
│
├ test: テスト関連
│ ├ testcases: 各モジュールのテストケース
│ │ ├ my_math_test.c
│ │ └ my_string_test.c
│ │
│ └ runner: 各テストケースのランナー
│ ├ my_math_test_runner.c
│ ├ my_string_test_runner.c
│ └ all_tests.c
│
└ CMakeLists.txt
もちろん、これは一例です。構成に指定があるわけではなく、どのようなかたちであっても必要なモジュールがビルドさえできれば OK です。
CMakeLists.txt は各フォルダーに分割して配置するほうが好ましいとは思いますが、今回は簡単にルートにてまとめて指定を行うようにします。
CMakeLists.txt
Unity の examples/
フォルダ には Makefile
が入っていて、これをマネすればビルドがらくちんです。が、 CMake にてビルドしたいので、マネしながら書いてみます。特に CMake の指定があるわけではなく、完全に趣味です。
cmake_minimum_required( VERSION 3.2 )
project( unity_test )
set( UNITY_ROOT lib/unity )
include_directories( ${UNITY_ROOT}/src )
include_directories( ${UNITY_ROOT}/extras/fixture/src )
include_directories( include/ )
link_libraries( m )
set(
UNITY_FILES
${UNITY_ROOT}/src/unity.c
${UNITY_ROOT}/extras/fixture/src/unity_fixture.c
)
set(
TEST_FILES
test/testcases/my_math_test.c
test/testcases/my_string_test.c
)
set(
SOURCE_FILES
src/my_math.c
src/my_string.c
)
set(
TEST_RUNNER_FILES
test/runner/my_math_test_runner.c
test/runner/my_string_test_runner.c
test/runner/all_tests.c
)
add_executable( unity_test ${SOURCE_FILES} ${TEST_FILES} ${TEST_RUNNER_FILES} ${UNITY_FILES} )
TEST_FILES
には 今回追加したテストケースを、TEST_RUNNER_FILES
には各テストケースのランナーとテストのランナーを指定します。SOURCE_FILES
には今回のテスト対象のモジュールを指定します。
実行
実行するとこんな感じになります。実装は空相当で、まだ何もしていないので、テストケースは fail しています:
Unity test run 1 of 1
.unity_test/test/testcases/my_math_test.c:24:TEST(MyMath, my_math_should_return_1_where_exponent_is_0):FAIL: Expected 1 Was -999999
.unity_test/test/testcases/my_math_test.c:33:TEST(MyMath, my_math_should_return_minus_1_where_exponent_is_negative):FAIL: Expected -1 Was -999999
.unity_test/test/testcases/my_math_test.c:47:TEST(MyMath, my_math_should_return_minus_100_where_exponent_is_larger_than_10):FAIL: Expected 1024 Was -999999
.unity_test/test/testcases/my_math_test.c:61:TEST(MyMath, my_math_should_return_powered_2_otherwise):FAIL: Expected 2 Was -999999
.unity_test/test/testcases/my_string_test.c:30:TEST(MyString, my_string_should_return_hello_where_0_is_given):FAIL: Expected '(^O^) < \0xE3\0x81\0x93\0xE3\0x82\0x93\0xE3\0x81\0xAB\0xE3\0x81\0xA1\0xE3\0x81\0xAF' Was ''
.unity_test/test/testcases/my_string_test.c:42:TEST(MyString, my_string_should_return_goodbye_where_1_is_given):FAIL: Expected '(T_T) < \0xE3\0x81\0x95\0xE3\0x82\0x88\0xE3\0x81\0x86\0xE3\0x81\0xAA\0xE3\0x82\0x89' Was ''
.unity_test/test/testcases/my_string_test.c:54:TEST(MyString, my_string_should_return_greeting_otherwise):FAIL: Expected '(\0xE3\0x83\0xBBv\0xE3\0x83\0xBB) < \0xE3\0x81\0x94\0xE3\0x81\0x8D\0xE3\0x81\0x92\0xE3\0x82\0x93\0xE3\0x82\0x88\0xE3\0x81\0x86' Was ''
-----------------------
7 Tests 7 Failures 0 Ignored
FAIL
Process finished with exit code 7
コマンドライン引数 -v
を指定するともう少し詳細が表示されます:
Unity test run 1 of 1
TEST(MyMath, my_math_should_return_1_where_exponent_is_0)unity_test/test/testcases/my_math_test.c:24:TEST(MyMath, my_math_should_return_1_where_exponent_is_0):FAIL: Expected 1 Was 999999
TEST(MyMath, my_math_should_return_minus_1_where_exponent_is_negative)unity_test/test/testcases/my_math_test.c:33:TEST(MyMath, my_math_should_return_minus_1_where_exponent_is_negative):FAIL: Expected -1 Was 999999
TEST(MyMath, my_math_should_return_minus_100_where_exponent_is_larger_than_10)unity_test/test/testcases/my_math_test.c:47:TEST(MyMath, my_math_should_return_minus_100_where_exponent_is_larger_than_10):FAIL: Expected 1024 Was 999999
TEST(MyMath, my_math_should_return_powered_2_otherwise)unity_test/test/testcases/my_math_test.c:61:TEST(MyMath, my_math_should_return_powered_2_otherwise):FAIL: Expected 2 Was 999999
TEST(MyString, my_string_should_return_hello_where_0_is_given)unity_test/test/testcases/my_string_test.c:30:TEST(MyString, my_string_should_return_hello_where_0_is_given):FAIL: Expected '(^O^) < \0xE3\0x81\0x93\0xE3\0x82\0x93\0xE3\0x81\0xAB\0xE3\0x81\0xA1\0xE3\0x81\0xAF' Was ''
TEST(MyString, my_string_should_return_goodbye_where_1_is_given)unity_test/test/testcases/my_string_test.c:42:TEST(MyString, my_string_should_return_goodbye_where_1_is_given):FAIL: Expected '(T_T) < \0xE3\0x81\0x95\0xE3\0x82\0x88\0xE3\0x81\0x86\0xE3\0x81\0xAA\0xE3\0x82\0x89' Was ''
TEST(MyString, my_string_should_return_greeting_otherwise)unity_test/test/testcases/my_string_test.c:54:TEST(MyString, my_string_should_return_greeting_otherwise):FAIL: Expected '(\0xE3\0x83\0xBBv\0xE3\0x83\0xBB) < \0xE3\0x81\0x94\0xE3\0x81\0x8D\0xE3\0x81\0x92\0xE3\0x82\0x93\0xE3\0x82\0x88\0xE3\0x81\0x86' Was ''
-----------------------
7 Tests 7 Failures 0 Ignored
FAIL
Process finished with exit code 7
適当な実装を行い、すべてのテストにパスしたときはこんな感じです。パスしたケースが .
で表示されます。シンプル:
Unity test run 1 of 1
.......
-----------------------
7 Tests 0 Failures 0 Ignored
OK
Process finished with exit code 0
コマンドライン引数 -v
を指定するとパス時にももう少し詳細が表示されます:
Unity test run 1 of 1
TEST(MyMath, my_math_should_return_1_where_exponent_is_0) PASS
TEST(MyMath, my_math_should_return_minus_1_where_exponent_is_negative) PASS
TEST(MyMath, my_math_should_return_minus_100_where_exponent_is_larger_than_10) PASS
TEST(MyMath, my_math_should_return_powered_2_otherwise) PASS
TEST(MyString, my_string_should_return_hello_where_0_is_given) PASS
TEST(MyString, my_string_should_return_goodbye_where_1_is_given) PASS
TEST(MyString, my_string_should_return_greeting_otherwise) PASS
-----------------------
7 Tests 0 Failures 0 Ignored
OK
Process finished with exit code 0
ちなみに付属の ruby スクリプトを使えば出力が色付きになったりします ( unity/auto/
参照 )。試してませんが……。
よく使用するアサーション一覧
よく使用するアサーションを一覧にしました。各ケースは関数に suffix _MESSAGE
をつけてメッセージを引数に与えると、fail 時にメッセージを表示させることができます ( ex.: TEST_ASSERT_EQUAL_MESSAGE( 2, add( 1 + 1 ), "うそっ" );
)
TEST_IGNORE()
テストケースをスキップする
TEST_FAIL()
テストケースを失敗させる
TEST_ASSERT_TRUE( cond )
条件 cond
が true
であると期待する
TEST_ASSERT_FALSE( cond )
条件 cond
が false
であると期待する
TEST_ASSERT_EQUAL( expected, actual )
actual
が expected
と同値であることを期待する。汎用的。
TEST_ASSERT_EQUAL_MEMORY( expected, actual, length )
配列である actual
と 配列 expected
が length
個一致することを期待する。便利ですが、当然境界には注意する必要があります。
TEST_ASSERT_EQUAL_STRING( expected, actual )
文字列 actual
と expected
が一致することを期待する。文字列は当然終端しておきます。
TEST_ASSERT_EQUAL_STRING_LEN( expected, actual, len )
文字列 actual
と expected
が len
文字一致することを期待します
TEST_ASSERT_FLOAT_WITHIN( delta, expected, actual )
単精度実数値 actual
と expected
が誤差 delta
の範囲内であることを期待します。つまり abs( actual - expected ) <= delta
です。
TEST_ASSERT_NULL( expression )
expression
が NULL
であることを期待する
TEST_ASSERT_NOT_NULL( expression )
expression
が NULL
で ない ことを期待する