HOME > ソフテックだより > 第293号(2017年11月1日発行) 技術レポート「アプリケーション開発におけるCppUnitの活用」

「ソフテックだより」では、ソフトウェア開発に関する情報や開発現場における社員の取り組みなどを定期的にお知らせしています。
さまざまなテーマを取り上げていますので、他のソフテックだよりも、ぜひご覧下さい。

ソフテックだより(発行日順)のページへ
ソフテックだより 技術レポート(技術分野別)のページへ
ソフテックだより 現場の声(シーン別)のページへ


ソフテックだより 第293号(2017年11月1日発行)
技術レポート

「アプリケーション開発におけるCppUnitの活用」

1. はじめに

これまで単体試験は人の手で行うことが多かったのですが、人の手で行うと、どうしても確認ミスや見逃しが発生します。また時間的な制約でテストパターンの網羅率が低くなってしまいます。そのため、単体試験を自動で行ってくれるテストフレームワークの活用が注目されています。
私が担当した案件で、組み込みアプリケーション開発で、テストフレームワークの一つであるCppUnitを使用して単体試験を行う機会がありましたので、紹介させていただきます。

2. 単体試験について

単体試験(内部試験やユニットテストと呼ばれることもあります)とは、一般的にプログラムを構成する個々のモジュールが正しく動作することを検証するテストです。試験対象としては、プログラムを構成するクラスなどのインターフェース(関数やメソッド)が対象となることが多いと思います。
単体試験を行うことで、問題を早期に発見することができ、後の工程で問題を発見するよりも容易に対応することができます。また単体試験を実施することにより、各モジュール単体の動作は正しいことが保証されます。そのため、それ以降の開発や結合試験の効率を高めることができます。

3. CppUnitによるユニットテスト概要

CppUnitは単体試験を自動化してくれるC++用のテストフレームワークです。
単体試験対象のクラスのメソッドを呼び出し、適切なパラメータを渡して、戻り値が期待された値かどうかを確認します。

今回、Xilinx SDKの環境でC++言語で記述したプログラムを作成し、そのプログラムに対してCppUnitを使用して単体試験を行いました。CalculateクラスをターゲットにCppUnitを使用して単体試験を行う場合を例にして、以下の説明を行います。

CppUnitを使用したアプリケーションの単体テストの構成図
図1. CppUnitを使用したアプリケーションの単体テストの構成図

4. CppUnitのXilinx SDKへの導入

CppUnitをXilinx SDKへ導入する手順を以下に示します。
今回使用する環境は以下の通りとします。

OS:Linux(CentOS 6.7)
開発環境:Xilinx SDK 2013.4
CppUnit:cppunit-1.12.1

4.1 CppUnitのダウンロード

以下のサイトからCppUnitダウンロードし、任意の場所に置きます。 https://sourceforge.net/projects/cppunit/cppunit/1.12.1/

4.2 CppUnitの解凍

CppUnitを解凍します。

[e-rt3@localhost ~]$ tar zxvf cppunit-1.12.1.tar.gz

解凍に成功すると「cppunit-1.12.1」フォルダが作成されます。

4.3 CppUnitのインストール

「cppunit-1.12.1」フォルダへ移動し、「./configure」スクリプトを実行し、Makefileを作成します。

[e-rt3@localhost ~]$ cd cppunit-1.12.1
[e-rt3@localhost cppunit-1.12.1]$ ./configure

「make」を実行し、CppUnitをビルドします。

[e-rt3@localhost cppunit-1.12.1]$ make

「make install」を実行し、CppUnitをインストールします。インストールはroot権限で実行します。

[root@localhost cppunit-1.12.1]# make install

インストールが完了すると、「/usr/local/lib」にCppUnitがインストールされます。

[root@localhost cppunit-1.12.1]# ls /usr/local/lib
libcppunit-1.12.so.1      libcppunit.a   libcppunit.so
libcppunit-1.12.so.1.0.0  libcppunit.la  pkgconfig

4.4 ライブラリパスの設定

CppUnitに対するライブラリパスを設定するため、「/etc/lc.so.conf」に「/usr/local/lib」を追加します。
「/etc/lc.so.conf」の編集はroot権限が必要になります。

[root@localhost cppunit-1.12.1]# vi /etc/lc.so.conf

編集前

include ld.so.conf.d/*.conf

編集後

include ld.so.conf.d/*.conf
/usr/local/lib

編集が完了したら、「ldconfig」コマンドで反映します。

[root@localhost cppunit-1.12.1]# ldconfig

今回使用したXilinxのlibstdc++のバージョンは3.4.8ですが、CppUnitで必要なバージョンは3.4.9以降になります。そのため、Xilinx環境のlibstdc++を更新します。/usr/lib/にバージョン3.4.9のlibstdc++がありましたので、それをコピーします。ちなみに、バージョンはstringsコマンドで確認することができます。

[root@localhost lin]# cd /opt/Xilinx/SDK/2013.4/lib/lin/
[root@localhost lin]# cp /usr/lib/libstdc++.so.6 .
cp: `./libstdc++.so.6' を上書きしてもよろしいですか(yes/no)? yes

Xilinx環境のlibstdc++.so.6を変更したので、デバッグ情報をインストールします。

  [root@localhost lin]# yum --enablerepo='*-debug*' install
/usr/lib/debug/.build-id/b9/32867feec9b1a3abc385246cda4d55a8023f5e.debug

4.5 ライブラリのリンカ設定

プロジェクトのリンカ設定にライブラリ(cppunitとdl)を登録します。
cppunitはCppUnitのライブラリです。
CppUnitではdlOpen等を使用するため、dlライブラリも必要です。

プロジェクトのリンカ設定
図2. プロジェクトのリンカ設定

以上でCppUnitの準備は完了です。

5. テストコードの記述

以下に、Calculateクラスのaddをテストする場合のテストコードを示します。

ターゲットクラス

// クラス宣言
class Calculate {
public:
  int add(int a, int b);
}

// 加算関数
int Calculate::add(int a, int b){
  return (a + b);
}

テストコード

#include 
#include 
#include 
#include 
#include 
#include 
#include 

class MyTest : public CPPUNIT_NS::TestFixture {
  CPPUNIT_TEST_SUITE(MyTest);	// テストケース開始の宣言
  CPPUNIT_TEST(test_add);		// テストケース
  CPPUNIT_TEST_SUITE_END();	// テストケース終了の宣言
public:
  virtual void setUp();		// テストケースの事前処理
  virtual void tearDown();	// テストケースの事後処理
protected:
  Calculate* mCalc;
  void test_add();
};
CPPUNIT_TEST_SUITE_REGISTRATION(MyTest);

// テストケースの事前処理
void MyTest::setUp(){
  mCalc = new Calculate();
}
// テストケースの事後処理
void MyTest::tearDown(){
  delete mCalc;
}
// add()のテストコード
void MyTest::test_add(){
  // add()に入力値1と2を与え、期待値3と比較する
  CPPUNIT_ASSERT_EQUAL(3, mCalc->add(1, 2));
}

// main()で登録されたテストをまとめて実行する
int main(int argc, char* argV[]){
  // イベント・マネージャとテスト・コントローラを生成する
  CPPUNIT_NS::TestResult controller;

  // テスト結果収集リスナをコントローラにアタッチする
  CPPUNIT_NS::TestResultCollector result;
  controller.addListener(&result);

  // 「.」で進行状況を出力するリスナをアタッチする
  CPPUNIT_NS::BriefTestProgressListener progress;
  controller.addListener(&progress);

  // テスト・ランナーにテスト群を与え、テストする
  CPPUNIT_NS::TestRunner runner;
  runner.addTest(CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest());
  runner.run(controller);

  // テスト結果を標準出力に吐き出す
  CPPUNIT_NS::CompilerOutputteroutputter(&result,CPPUNIT_NS::stdCOut());
  outputter.write();

  return result.wasSuccessful() ? 0 : 1;
}
上記test_add()がテストコードの実装部分です。setUp()とtearDown()はテストケースの事前処理と事後処理になります。その他の説明はここでは省略します。

6. テスト実行

上記テストコードを実行すると、Xilinx SDKのコンソールにテスト結果を表示します。

テスト成功時

MyTest::test_add : OK
OK (1)

テスト失敗時

MyTest::test_add : assertion
Test name: MyTest::test_add
equality assertion failed
- Expected: 1
- Actual  : 0

Failures !!!
Run: 1   Failure total: 1   Failures: 1   Errors: 0

7. CppUnitを使用した感想

CppUnitを使用した感想を以下に示します。
なお、テストフレームワークは、CppUnit以外にもあります。テストフレームワークを使用する場合の一般的な利点や注意点は『第277号:WindowsアプリケーションにおけるMSTestの活用』を参照願います。

(1) CppUnitを導入するまでが大変

今回、LinuxのXilinx SDKにCppUnitを組み込みましたが、当時はそれほどLinuxに精通しているわけではありませんでした。そのため、導入に至るまでにかなり苦労したことを記憶しています。特に、libstdc++のバージョンが異なる問題については、色々と試行錯誤を繰り返しました。導入までは、ある程度Linuxに慣れている必要があると思います。
一旦導入してしまえば、テストコードの修正・追加やテスト実行はLinuxを意識せずに容易に行うことができます。

(2) 全てのテストケースに対応するテストコードを一つひとつ作成する必要がある

上記テスト実行結果を見て分かるように、テスト結果の出力はテストコードの関数名に対してOKやassertionを出力します。通常、試験対象の1関数に対してテストケースは複数存在します。1つのテストコードに複数のテストケースを記述することもできますが、テスト結果は1つしか出力しないため、テスト失敗時にどのテストケースで失敗したのかわかりません。
そのため、今回は一つひとつのテストケース毎にテストコードを作成しました。テストコードは全て手打ちで作成する必要があり、これがなかなか大変でした。
例えば100個の関数をテストし、それぞれの関数のテストパターンが10個あるとします。この場合、1000個のテストコードを作成しなければいけません。テストコード自体はほとんど同じでコピー&ペーストでいけますが、テストコードが大量になり、同じようなテストコード複数存在する等、テストコードの管理が大変でした。
Visual StudioのMSTestでは、1つのテストコードで複数の入力値と期待値を入力したファイルを読み込んでテストできる機能があります。残念ながら、CppUnitにはその機能はありません。Visual Studioで開発する場合は、MSTestを使用してみても良いかも知れません。
ただし、メッセージを出力させるなど、テスト方針や工夫次第で、必ずしも一つひとつのテストケース毎にテストコードを作成しなければいけないわけではないことを付け加えておきます。

(3) 試験項目書とのマージが大変

今回、試験項目書を作成し、それを元にテストコードを作成しました。試験項目書の作成自体も大変でしたが、仕様変更や不適合でプログラムを修正すると、当然、テストコードと共に試験項目書も修正する必要があります。また、どのリビジョンのテストを実施したのかを示す構成管理も重要です。
このようなことを考慮しつつ、出来るだけ対応が容易で時間がかからない方法を構築することが今後の課題と考えています。

(4) テスト失敗時に失敗理由がわからない

上記のように、CPPUNIT_ASSERT_EQUAL()を使用すると、テストに失敗しても「assertion」としか表示されません。少なくとも、期待値と実際に出力した値は表示して欲しいものです。そのためには、CPPUNIT_ASSERT_EQUAL_MESSAGE()を使用し、メッセージを出力させることで対応可能ですが、メッセージは自力で作成しなければいけません。

(5) テストの実行が楽

CppUnitの導入やテストコードを作成するまでは、確かに時間やコストがかかります。しかし、一旦テストコードを作成してしまえば、後は何度でもテストを実行することができます。ターゲットアプリケーションに何度か変更し、その度に単体試験を実行したのですが、人手で行えば何十時間もかかるような単体試験を、自動で簡単にできたことは、それまでにかかった手間やコストを十分に回収できたと思います。

8. 最後に

アプリケーション開発でCppUnitを活用する方法を紹介させていただきました。
CppUnitというテストフレームワークを活用することで、これまで単体試験を人手で行っていたことで生じる各種問題を低減できる可能性があることを感じました。それと同時に、このテストフレームワークを有効活用するための方法を模索していかなければいけないとも感じています。
また、今回はCppUnitを使用しましたが、Visual StudioのMSTest等、テストフレームワークは他にもあります。開発環境やどのようなテストをするかに応じて、適切なテストフレームワークを選択しなければいけません。
今回紹介させていただいた内容が、アプリケーション開発時の品質向上の手助けになることが出来れば幸いです。

(T.M.)

[参考サイト]
C++アプリケーションの効率的なテスト手法(CppUnit編)

関連ページへのリンク

関連するソフテックだより

ページTOPへ