第2回 C++アプリケーションの効率的なテスト手法(CppUnit編):連載 C++開発者のための単体テスト入門(2/4 ページ)
効率的なテスト手法の1つが単体テストだ。CppUnitというフレームワークの活用例を通して単体テストの有効性を理解しよう。
■CppUnitテスト環境の構築
では、CppUnitを使った単体テストの方法を、順を追って説明します。簡単なカウンタ・クラス(=カウントを数える機能を持つクラス)をテスト対象としましょう。
さっそくVS 2005でソリューション「CounterSolution」を作成し、その中にテスト対象となる(「Win32」の)スタティック・ライブラリのプロジェクト「Counter」を用意します(※プロジェクトの作成方法については、前回の記事などを参照してください)。
そしてもう1つ、Counterプロジェクトをテストする「Win32 コンソール アプリケーション」のプロジェクトとして、「CounterTest」をCounterSolutionソリューションに追加します(※[プリコンパイル済みヘッダー]は不要です。いったん自動生成されたすべてのファイルは削除しておいてください)。こちらはCounterプロジェクトで生成されるライブラリを利用するアプリケーションなので、「CppUnit導入手順」で説明したCppUnitTestMainプロジェクトと立場的(?)には同じです。よって、先ほどのCppUnitTestMainプロジェクトと同様の手順で、CounterTestプロジェクトをスタートアップ・プロジェクトとし、テスト対象であるCounterプロジェクトへの依存関係を設定します。
テスト用のCounterTestプロジェクトは、ビルドと実行に単体テスト・フレームワークのCppUnitを必要とします。そこで、メニュー・バーから[ツール]−[オプション]を選択して[オプション]ダイアログを表示して、次の画面のように、[プロジェクトおよびソリューション]−[VC++ ディレクトリ]を選択し、[ディレクトリを表示するプロジェクト]を切り替えて、「インクルード ファイル」と「ライブラリ ファイル」のそれぞれにCppUnitのヘッダ・ファイルとライブラリ・ファイルが置かれたディレクトリを追加してください。
テスト用プロジェクトでのCppUnitのヘッダとライブラリに関する設定
ここでは、「インクルード ファイル」のディレクトリ設定として、CppUnitのヘッダが格納されているディレクトリ「<CPPUNIT_PATH>\include」を追加している。さらに、「ライブラリ ファイル」のディレクトリ設定では、ディレクトリ「<CPPUNIT_PATH>\lib」を追加すればよい。
CounterTestプロジェクトには、Counterプロジェクトが提供するヘッダ・ファイルの在りかを([追加のインクルード・ディレクトリ]で)指定し、CppUnitのライブラリ・ファイルを([追加の依存ファイル]で)リンクしなければなりません。
そこでまずは、次の画面のように、([ソリューション エクスプローラ]でCounterTestプロジェクトを右クリックして[プロパティ]を選ぶと表示される)[CounterTest プロパティ ページ]ダイアログの[構成]から「Debug」を選び、左側のツリー表示から[構成プロパティ]−[C/C++]−[全般]を選択し、右側にある[追加のインクルード ディレクトリ]にヘッダ位置(本稿の例では「../Counter」)を設定します。
CounterTestプロジェクトに対するインクルード・ディレクトリの追加
Counterプロジェクトが提供するヘッダ・ファイルが格納されているディレクトリ(この例では「../Counter」)を、インクルード・ディレクトリとして追加する。
次に、同じく[CounterTest プロパティ ページ]ダイアログの[構成プロパティ]−[リンカ]−[入力]を選択し、[追加の依存ファイル]に適切なCppUnitライブラリ(本稿の例ではデバッグ版のスタティック・ライブラリである「cppunitd.lib」)を設定します。
これでCppUnitによるテスト環境が整いました。かなり面倒ではありますが、最初に一度だけやればいいことですので、快適なテスト環境のためにちょっとだけ辛抱してください。
それではカウンタ・クラスを作成し、それをCppUnitでテストするコードを実装しましょう。
■カウンタ・クラスの作成
カウンタ・クラスはCounterプロジェクト内に作成します。そのカウンタ・クラスは、以下の仕様を満たすものとします。
クラス名:Counter
クラスの仕様:
・Counterクラスはカウント値(=int型の変数)を内包する
・カウント値の初期値は「0」である
・getメソッドは現在のカウント値を返す
・incrメソッドによってカウント値を+1する
・clearメソッドによってカウント値を初期値(=0)に設定する
この仕様から、次のようなヘッダ・ファイル(Counter.h)が作られるでしょう。
#ifndef COUNTER_H__
#define COUNTER_H__
class Counter {
private:
// プライベート変数
int count_; // カウント値(=int型の変数)を内包する
public:
// コンストラクタ
Counter(); // カウント値の初期値は「0」である
// メソッド
int get() const; // 現在のカウント値を返す
void incr(); // カウント値を+1する
void clear(); // カウント値を初期値(=0)に設定する
};
#endif
Counterクラスの各メソッドは、この段階ではまだコンパイル/リンクできるだけの形式的なもの(=ハリボテ)で十分です。そこで、このヘッダに対する実装(Counter.cpp)を次のように記述します。
#include "Counter.h"
Counter::Counter() {
}
int Counter::get() const {
return -123;
}
void Counter::incr() {
}
void Counter::clear() {
}
この2つのファイルをCounterプロジェクトに追加し、ビルドに成功することを確認しておきましょう。
■CppUnitテストの実装と実行
ここからテストの作成が始まります。
CppUnitを使ったテスト・コードには、守らなければならないいくつかの“約束”があります。ともかくテスト・コードをご覧いただきましょうか。
#include <cppunit/extensions/HelperMacros.h> // (1)
#include "Counter.h"
// 以下はCounterTestクラスの宣言-----
class CounterTest : public CPPUNIT_NS::TestFixture { // (2)
CPPUNIT_TEST_SUITE( CounterTest ); // (3)
CPPUNIT_TEST( test_init ); // (4)
CPPUNIT_TEST( test_incr ); // (4)
CPPUNIT_TEST( test_clear ); // (4)
CPPUNIT_TEST_SUITE_END(); // (5)
protected:
Counter* c_;
public:
void setUp(); // (6)
void tearDown(); // (6)
protected:
void test_init(); // (7)
void test_incr(); // (7)
void test_clear(); // (7)
};
// 以下はCounterTestクラスの実装-----
CPPUNIT_TEST_SUITE_REGISTRATION( CounterTest ); // (8)
// (6)各テスト・ケースの実行直前に呼ばれる
void CounterTest::setUp() {
c_ = new Counter();
}
// (6)各テスト・ケースの実行直後に呼ばれる
void CounterTest::tearDown() {
delete c_;
}
// (7)これ以降はテスト・ケースの実装内容
void CounterTest::test_init() {
CPPUNIT_ASSERT_EQUAL(0, c_->get()); // (9)
}
void CounterTest::test_incr() {
for ( int i = 1; i < 10; ++i ) {
c_->incr();
CPPUNIT_ASSERT_EQUAL(i, c_->get()); // (9)
}
}
void CounterTest::test_clear() {
if ( c_->get() == 0 ) {
c_->incr();
}
c_->clear();
CPPUNIT_ASSERT_EQUAL(0, c_->get()); // (9)
}
(1) CppUnitには、記述の簡略化のためのマクロが多数用意されており、HelperMacros.hファイルをインクルードすることでそれらが使えるようになります。
(2) CPPUNIT_NS::TestFixtureクラスを継承したクラス(「テスト・フィクスチャ」と呼ばれる)はテストの大分類に相当し、このクラスに一連のテストをまとめます。ここでは、「Counterクラスに関する一連のテスト」ということで、クラス名を「CounterTest」としました。
(3) CPPUNIT_TEST_SUITE( CounterTest )マクロが、CounterTestクラスに用意されたテスト(以後、テスト・スイート)の宣言開始を示します。マクロ・パラメータ(この例では「CounterTest」)はクラス名と一致させてください。
(4) CPPUNIT_TEST( <メソッド名> )マクロによって、そのメソッドがテスト項目(CppUnitでは「テスト・ケース」と呼ぶ。以後、テスト・ケース)であることを表します(この例では「test_init」「test_incr」「test_clear」という3つのメソッドをテスト・ケースとして列挙しています)。
(5) CPPUNIT_TEST_SUITE_END()マクロで、テスト・スイートの終端(=テスト・ケースの列挙の終了)を表現します。マクロ・パラメータはありません。
(6) setUp/tearDownメソッドは、それぞれ各テスト・ケースの直前/直後に行う処理を定義し、これを用いてテスト・ケースごとの初期化と後始末ができます。
(7) テスト・ケースとなるメソッド(=(4)で指定されたメソッド)には、パラメータも戻り値もありません。各メソッドの中でテスト対象を呼び出して“期待される結果”が得られるかを検証します。テスト結果の検証のために、いくつかのマクロが用意されています。その中の代表的なものを挙げておきましょう。
- CPPUNIT_ASSERT( condition );
conditionにはテストの結果が正しければ真(=true、非0)となる式を記述します。例えば、テストの結果で変数「x」が「0以上」であることを期待するなら、次のように書きます。
CPPUNIT_ASSERT( x >= 0 ); - CPPUNIT_ASSERT_EQUAL( expected, actual );
expectedに期待する値を、actualに実際の値を指定します。両者が等しければテスト成功です。 - CPPUNIT_ASSERT_DOUBLES_EQUAL( expected, actual, delta );
期待する値(=expected)と実際の値(=actual)との差が、deltaに指定された値以下であれば、テスト成功とします。 - CPPUNIT_FAIL( message );
必ず失敗します。失敗時にmessageに指定された文字列を出力します。これは主に、正常ならば絶対に通ることのないパスに書きます。例えば「f()」という関数が「0」か「1」を返し、それ以外の値を取り得ないのであれば、次のコードのように記述します。
switch ( f() ) {
case 0: … ; break;
case 1: … ; break;
default: CPPUNIT_FAIL(“shouldn’t be reached!!!”);
}
これら検証マクロは1つのテスト・ケースの中にいくつ書いても構いませんが、その中でテストに失敗すると、それ以降のテストは行われず、次のテスト・ケースに実行が移ります。
(8) CPPUNIT_TEST_SUITE_REGISTRATION( <テスト・スイート名> )マクロによって、(3)〜(6)で定義されたテスト・スイートをテスト対象として登録します。テスト・スイート名には(3)で指定した名前(=クラス名)を使います。
以上のルールに従って実装されたクラスは1つのプロジェクトに複数用意することができ、登録されたテストはすべて実行されます。
最後に残るのがmain関数です。
Copyright© Digital Advantage Corp. All Rights Reserved.