EurekaMoments

ロボットや自動車の自律移動に関する知識や技術、プログラミング、ソフトウェア開発について勉強したことをメモするブログ

CUnitを使ってC言語プログラムのユニットテストを実行する方法

はじめに

以前、C言語で書いたプログラムをPythonから呼び出してユニットテストを実行する方法として、下記の記事を書きました。

www.eureka-moments-blog.com

これはこれで良いのですが、Cythonのコードを書いてビルドできるようにするまでの準備が面倒だなとも感じており、もっと簡単に実行できる方法はないか調べてみました。その結果、「CUnit for Mr.Ando」というフリーのフレームワークを利用して、C言語だけでユニットテストが出来る環境を作ることが出来ることを知りました。
今回見つけた上記の方法を、簡単なサンプルプログラムを作って試してみたので、そのやり方について紹介しようと思います。

経験ゼロでもできるプログラミング現場の単体テスト

経験ゼロでもできるプログラミング現場の単体テスト

CUnit for Mr.Andoについて

「CUnit for Mr.Ando」(以下、CUnit)とは、安藤利和さんという方が作り、下記のサイトにて公開されているフリーのC言語テスティングフレームワークです。

park.ruru.ne.jp

もともとJavaでは、JUnitという強力な単体テストツールがありますが、C言語でもそういうテストツールがあるべきだと考え、下記のことを意識して作成されたそうです。

f:id:sy4310:20190216213453p:plain

以前自分が紹介したようにPython, Cythonといった異なる言語を組み合わせる必要がなく、C言語だけを使って簡単にユニットテストが実行できるようになるということで、非常に魅力的ですね。
自分個人だけならあまり気にしませんが、仕事ではチームで開発するので、誰もが分かりやすく、出来るだけ簡単に実行できる方法を取り入れたいものです。

CUnitの導入からサンプル実行までの流れ

1. CUnitのダウンロード

CUnitは下記のサイトからダウンロードすることが出来ます。必要なソースコード一式がzipファイルでダウンロードされるので、あとはそれを好きな場所に展開しておけばOKです。

ja.osdn.net

2. ダウンロードされるファイルの内容

ダウンロードされるファイル一式と、それらのフォルダ構成は下記の通りになっています。

f:id:sy4310:20190216220842p:plain

この構成の中にある、CUnitForAndoというフォルダにフレームワーク本体のソースコードが置かれています。ソースコードは、testRunner.c/hの2つであり、それぞれ下記のように記述されています。

  • testRunner.h
/*
 * "CUnit for Mr.Ando" is CppUnit-x based C langage testing framework
 * for Mr.Ando. It provide the C source code for unit testing. 
 * Copyright (C) 2004-2005 Toshikazu Ando.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#ifndef _TEST_RUNNER_H_
#define _TEST_RUNNER_H_
#include <stdio.h>

#ifdef __cplusplus
extern "C" {
#endif

/** Assert single */
#define TEST_ASSERT(_a) \
    {\
        int assertErrorA = (_a);\
        if (! assertErrorA) {\
            fprintf(stdout,"%s:%d: error: TestCaseError(0x%08x)\n",\
                __FILE__,__LINE__,(assertErrorA));\
            return 0xffffffffu;\
        } else {\
            doTestCountUp();\
        }\
    }

/** Assert equals */
#define TEST_ASSERT_EQUALS(_a,_b) \
    {\
        int assertErrorA = (_a);\
        int assertErrorB = (_b);\
        if ((assertErrorA) != (assertErrorB)) {\
            fprintf(stdout,"%s:%d: error: TestCaseError<0x%08x><0x%08x>\n",\
                __FILE__,__LINE__,(assertErrorA),(assertErrorB));\
            return 0xffffffffu;\
        } else {\
            doTestCountUp();\
        }\
    }

/** Assert equals */
#define TEST_ASSERT_NOT_EQUALS(_a,_b) \
    {\
        int assertErrorA = (_a);\
        int assertErrorB = (_b);\
        if ((assertErrorA) == (assertErrorB)) {\
            fprintf(stdout,"%s:%d: error: TestCaseError<0x%08x><0x%08x>\n",\
                __FILE__,__LINE__,(assertErrorA),(assertErrorB));\
            return 0xffffffffu;\
        } else {\
            doTestCountUp();\
        }\
    }

/** File pointer define */
typedef unsigned int (*TEST_FUNCTION)(void);

/** Test runner. */
unsigned int testRunner(TEST_FUNCTION testFunction);

/** Test counter */
void doTestCountUp(void);

#ifdef __cplusplus
}
#endif
#endif 
  • testRunner.c
/*
 * "CUnit for Mr.Ando" is CppUnit-x based C langage testing framework
 * for Mr.Ando. It provide the C source code for unit testing. 
 * Copyright (C) 2004-2005 Toshikazu Ando.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
#include <stdio.h>
#include "testRunner.h"

static unsigned int counter;

unsigned int testRunner(TEST_FUNCTION testFunction) {
    int err;
    counter = 0;
    err = (testFunction)();
    if (err) {
        printf("NG (now %d ok...)\n",counter);
        return 0xffffffffu;
    }
    printf("OK (%d tests)\n",counter);
    return 0;
}

void doTestCountUp(void) {
    counter++;
}

3. CUnitの使い方

上記のtestRunner.c/hのコードを見れば分かるように、CUnitでは下記の3パターンのテストを実行できるようになっています。

f:id:sy4310:20190216222639p:plain

大まかな使い方としては、

  • まずは、テスト対象とするコードを書く。(このコードに対するヘッダファイルも必ず作る)
  • ヘッダファイルには、2重インクルード防止のために記述を書くことを忘れない。
  • テストを実行するコードを書く。
  • テストを実行する関数名は、test + 関数名とする。
  • テストコードのファイル名は、ファイル名 + Testとする。
  • テストコードの方で、testRunnerとテスト対象コードをインクルード。
  • 必要なコードをmakeして、テスト実行。

といった感じになります。詳しい使い方は下記にて書かれているので参照ください。

park.ruru.ne.jp

4. テストの対象とするコードを書く

今回はサンプルプログラムとして、入力引数として与えた2つの数の差分を計算する引き算関数(Minus.c/h)を作りました。
この引き算関数は、入力引数と計算結果の値に制限を設けており、いずれかの値が-100以下になると、例外処理として1を返します。また、いずれの値も-100以下にならなければ、引き算を計算して、最後に0を返します。

  • Minus.h
#ifndef _MINUS_H_
#define _MINUS_H_

unsigned int Minus(
    int data1,
    int data2,
    int *ans);

#endif
  • Minus.c
#include "Minus.h"

unsigned int Minus(
    int data1,
    int data2,
    int *ans)
{
    int min = -100;
    *ans = data1 - data2;
    if ((min >= data1) || (min >= data2) || (min >= *ans))
    {
        return 1;
    }
    return 0;
}

5. テストコードを書く

今回書いたサンプルのテストコードは、上記の3にて示した3パターンのテストの内、TEST_ASSERT_EQUALとTEST_ASSERTの2パターンを実行します。これにより、引き算が正しく計算出来ていること、例外処理が正しく行われていることを確認できます。サンプルコードは下記の通りです。

#include <stdio.h>
#include <testRunner.h>
#include "Minus.h"

static unsigned int TestMinus(void);

/** Main function. */
int main(void)
{
    return (int) testRunner(TestMinus);
}

static unsigned int TestMinus(void)
{
    unsigned int err;
    int ans;
    int data1;
    int data2;
    
    // Test case 1, 2
    data1 = 5;
    data2 = 20;
    err = Minus(data1,data2,&ans);
    TEST_ASSERT_EQUALS((data1 - data2),(int)ans);
    TEST_ASSERT_EQUALS(err,0);

    // Test case 3, 4
    data1 = 84;
    data2 = 97;
    err = Minus(data1,data2,&ans);
    TEST_ASSERT_EQUALS((data1 - data2),(int)ans);
    TEST_ASSERT_EQUALS(err,0);
    
    // Test case 5
    data1 = -100;
    data2 = 1;
    err = Minus(data1,data2,&ans); // do Test
    TEST_ASSERT(err != 0);

    // Test case 6
    data1 = 0;
    data2 = -200;
    err = Minus(data1,data2,&ans); // do Test
    TEST_ASSERT(err != 0);

    // Test case 7
    data1 = -1;
    data2 = 99;
    err = Minus(data1,data2,&ans); // do Test
    TEST_ASSERT(err != 0);
    
    return 0;
}

6. ソースコードのフォルダ構成を決める

今回のサンプルを実行するためのフォルダ構成は下記のようにしました。

f:id:sy4310:20190217171010p:plain

まず、フレームワークであるtestRunner.c/hを置くフォルダであるTestRunnerを作り、それと同じ階層に、テスト対象コードとテストコードを置くフォルダ(今回は引き算なので、MinusTest)を作ります。MinusTestフォルダの中では、対象コードを置くTargetと、テストコードを置くTestの2つのフォルダを作るようにして、対象コードとテストコードは分けて置くようにしています。

7. Makefileを書く

上記の6で決めたフォルダ構成に基づいて、テストコードをmake、実行するためのMakefileを下記のように書きます。

# "CUnit for Mr.Ando" is CppUnit-x based C langage testing framework
# for Mr.Ando. It provide the C source code for unit testing. 
# Copyright (C) 2004 Toshikazu Ando.
# 
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# 
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

#CC=lint
CC=gcc
INCLUDE_OBJ =\
   -I../../TestRunner\
   -I../Target\
   -I./include
OBJS = \
   Minus.o\
   MinusTest.o\
   testRunner.o
CFLAG_LM  = ${CFLAG} -ggdb -Wall
CFLAG_OBJ = ${CFLAG} -ggdb -Wall

#
LIB_X   = ${LIB}

LM = ./runExe.exe

#
all: ${LM}
#
${LM}: ${OBJS}
    ${CC} ${CFLAG_LM} ${CFLAG_OBJ} ${INCLUDE_OBJ} -o ${LM} ${OBJS} ${LIB_X}
#
Minus.o: ../Target/Minus.c
  ${CC} ${CFLAG_OBJ} ${INCLUDE_OBJ} -c ../Target/Minus.c
MinusTest.o: ./MinusTest.c
  ${CC} ${CFLAG_OBJ} ${INCLUDE_OBJ} -c ./MinusTest.c
testRunner.o: ../../TestRunner/testRunner.c
  ${CC} ${CFLAG_OBJ} ${INCLUDE_OBJ} -c ../../TestRunner/testRunner.c
#
#
clean:
  rm -rf ${LM} ${OBJS}
#
run: all
  ${LM}
#

8. テストコードのmakeと実行

7で作成したMakefileは、テストコードを置いたTestフォルダの中に置きます。そして、そのフォルダまで移動して、make runコマンドを実行すると、テストコードがコンパイルされ、テストが実行されます。実行結果は下記のようになります。

f:id:sy4310:20190217172959p:plain

一番最後に、"OK (7 tests)"と表示されていますが、この場合は全部で7パターンのテストを実行して、全て期待通りの結果となった、ということです。ちなみに、テスト対象コードであるMinus.cの引き算の式をわざと足し算にした場合は、下記のような結果になります。

f:id:sy4310:20190217173639p:plain

引き算を想定しているので、足し算とでは計算結果が合わずにNGとなります。こういう時は、テストコードの該当部分でTestCaseErrorがあることが報告されます。

感想

Cythonを利用したやり方よりも大分楽にC言語のユニットテストを実行することが出来るようになりました。特にハマるようなポイントも無かったので、さすが「簡単である」と謳っているいるだけありますね。もっと複雑なテストケースも実行することができそうなので、これから試してまた紹介したいと思います。

サンプルコード

今回載せたサンプルコードは、下記のGitHubリポジトリで公開しています。

github.com