LoginSignup
31
28

More than 5 years have passed since last update.

ゲームじゃないほうの Unity で C 言語でテスト

Last updated at Posted at 2015-06-27

Unity とは?

「Unity ってゲームのやつかな? でも C言語?」とお思いでしょうが、ここでの UnityC 言語のためのテストフレームワークのほうです。テストケースを書いて、赤色を緑色にしていくやつですね。

小さくて手軽で書きやすいので、簡単にテストを書いて実行することができます。アサーションも必要なものがそろっています ( のちほどご説明 )。

以下では Unity の導入とテストケースの作成、ビルドと実行についてメモします。

導入

  1. Unity を入手します: https://github.com/ThrowTheSwitch/Unity
  2. プロジェクトの lib 等、好きな場所に置きます
  3. テストケースを書きます
  4. テストランナーを書きます
  5. モジュールを空実装します
  6. ビルドの設定をします
  7. テストを実行して真っ赤にします (なりませんが……)
  8. モジュールを修正します
  9. 緑にします

試す

お試しに適当なモジュールを作って、Unity でテストしてみます。

  1. 最近 2 の n 乗をよく求めるので 2 の n 乗する関数を作ろう ---( 機能A: my_math )
  2. 顔文字であいさつ定型文を出すやつを作ろう ---( 機能B: my_string )

これらの機能に対して、次の条件を満たすか適当な範囲をチェックしてみます:

  • 機能A: my_math

    1. 0 が与えられたときは常に定数 1 を返そう
    2. 負の数が与えられたときはわかんないのでとにかく -1 を返そう
    3. 10 以上はヤバそうなので -100 を返そう
  • 機能B: my_string

    1. あいさつは 3 種類用意
    2. 0 のときは "(^O^) < こんにちは"
    3. 1 のときは "(T_T) < さようなら"
    4. それ以外はすべて "(・v・) < ごきげんよう"

テストケースを作る

テストケースはテストグループを作成し、その中でテストケースの処理を定義してまとめて定義できます。

各テストは TEST_GROUP( group ) でテストケースのグループを作ります。group はテストグループ名で、グループを指定する際に使用します。

TEST_SETUP( group ) マクロと TEST_TEAR_DOWN( group ) マクロを使用することで、各テストケースの開始と終了時に処理を行うことができます。が、今回は空で置いておきます。

TEST( group, case )group に属したテストケースを定義していきます。

機能A のテストです。ここでは単純な整数値の一致のアサーションを使用します:

my_math_test.c
#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 のテストです。ここでは文字列の処理なので、文字列のアサーションを使用します:

my_string.c
#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_1example_3 では ruby で自動でランナーを作成するやり方があるので、それを改変することもできます ( ここでは説明は省きます )。

各テストに関してテストランナーを作ります:

my_math_test_runner.c
#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 );
}
my_string_test_runner.c
#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 );
}

すべてのテストケースのランナーを作ります:

all_tests.c
#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 );
}

構成

今回のプロジェクトはこんな感じのフォルダー構成にします。:

project
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 の指定があるわけではなく、完全に趣味です。

CMakeLists.txt
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 しています:

stdout
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 を指定するともう少し詳細が表示されます:

stdout
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

適当な実装を行い、すべてのテストにパスしたときはこんな感じです。パスしたケースが . で表示されます。シンプル:

stdout
Unity test run 1 of 1
.......

-----------------------
7 Tests 0 Failures 0 Ignored 
OK

Process finished with exit code 0

コマンドライン引数 -v を指定するとパス時にももう少し詳細が表示されます:

stdout
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 )

条件 condtrue であると期待する

TEST_ASSERT_FALSE( cond )

条件 condfalse であると期待する

TEST_ASSERT_EQUAL( expected, actual )

actualexpected と同値であることを期待する。汎用的。

TEST_ASSERT_EQUAL_MEMORY( expected, actual, length )

配列である actual と 配列 expectedlength 個一致することを期待する。便利ですが、当然境界には注意する必要があります。

TEST_ASSERT_EQUAL_STRING( expected, actual )

文字列 actualexpected が一致することを期待する。文字列は当然終端しておきます。

TEST_ASSERT_EQUAL_STRING_LEN( expected, actual, len )

文字列 actualexpectedlen 文字一致することを期待します

TEST_ASSERT_FLOAT_WITHIN( delta, expected, actual )

単精度実数値 actualexpected が誤差 delta の範囲内であることを期待します。つまり abs( actual - expected ) <= delta です。

TEST_ASSERT_NULL( expression )

expressionNULL であることを期待する

TEST_ASSERT_NOT_NULL( expression )

expressionNULLない ことを期待する

31
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
31
28