CppUnitの導入で得られた恩恵


注:最近は、GoogleTestを用いています。単体テストを自動化するツールの考え方は基本同じなので、以下の文章はそのままにしておきます。

単体テストとしてCppUnitを導入した。CppUnitを導入したことによる付加的な恩恵について紹介する。

準備

・CppUnitのライブラリのインストール
(まずはwebを参考してください)
 (必要なのはビルド済みのライブラリとヘッダファイルです。)
・CppUnitを用いたサンプルの単体テストをとにかく動かしてみる。
・何か1つテスト項目を増やしてみる。
 (ここまでたどりつけば何とかなります。くだらないテストでかまいませんから、何か一つ実装してみてください。)

class SampleTest : public CPPUNIT_NS::TestFixture{
    CPPUNIT_TEST(test_someTest);//メンバ関数の登録

protected:
  void test_someTest();//メンバー関数の宣言
}

void SampleTest::test_someTest(){//メンバー関数の実装
......
CPPUNIT_ASSERT(1==2);//必ず失敗する項目の例
}

CppUnitの利用作業

CppUnitのサンプルに1つテスト項目を追加した後の作業です。

提案するディレクトリ構成

・開発ターゲットのプロジェクト1式と開発ターゲットの単体テスト群の1式とがディレクトリ構成として並列になるようにしておく。testを実行するためにだけ必要なデータは、開発ターゲットの単体テストのディレクトリの中にtestDataというディレクトリにおきます。

(targetProject)/            開発ターゲットのプロジェクト
(UnitTest_targetProject)/       単体テスト群
(UnitTest_targetProject)/testData   単体テストで使用するデータ
(UnitTest_targetProject)/Debug Debugモードのexeが置かれる場所
(UnitTest_targetProject)/Release    Releaseードのexeが置かれる場所

テスト項目のCppUnitへの追加

以下の恩恵の受けられるように、ソースコード中のテストをCppUnitの枠組みに移行していく。

恩恵1:ソースコードを最終ターゲット用、テスト用に分離できる。

 上記の設定で、単体テストを記述するソースコード、実行のためのデータ環境、単体テストの動作結果を開発ターゲットのプロジェクトに含めないようにできます。
(開発元の環境においては多数のテストを行えますが、実装先においては、それらの単体テストを必要としないことも多いはずですし、場合によっては、実装先にそれらのテストの内容を公開したくない場合もあるはずです。単体テストのディレクトリは、開発ターゲットのディレクトリの外におくようにします。開発ターゲットのプロジェクトは、単体テスト側のファイルをなんら参照しないように作っておき、開発ターゲット側で完結するようにしておきます。)

恩恵2:開発済みのテストが将来もテストの継続がされる。

モジュールの開発時に書いていたテストコードをCppUnitに移す。
(まずは、既存の単体テストのサンプルにそのまま追加してみる。
テストする関数がfuncA()だったら、
test_funcA()
として、何をテストする、メンバー関数なのかがわかりやすくする。
 単体テストで必要とするデータもディレクトリの中で管理します(例:testData/lena.jpg)。 先ほどのディレクトリ構成にしておけば、DebugモードReleaseモードでも"../testData/lena.jpg"という形でアクセスできます。。こうすることで、DebugやReleaseの下にテストデータを別々に置かなくて済むようになります。
 funcA()を実行した結果はこうなっているはずだということを
CPPUNIT_ASSERT(論理式);
で書いていきます。

恩恵3:ターゲットのソースコードに#if DEBUGなどの条件付コンパイルのコードを書かなくてよくなる。

#if DEBUG #endifで囲まれた範囲のコードをテスト用のコードとして、単体テスト用のコードに移していく。そうすることで、開発ターゲットのコードの中には#if DEBUGのコードはなくなるし、単体テストの中で常にテストされることで、テストが確実に実行されるようになる。

(デバッグ用としているコードの中でも、単体テストで成否を判定する目的に置き換えればよいコードと、開発ターゲットプログラムの動作の解析のためのプログラムの中に残しておいた方がよいコードとがある。後者の場合には、単体テストの枠組みには移行させない。)

恩恵4:テスト用の関数が、ターゲットのソースコードから除去できる。

 このため、Doxygenで生成されたcall treeも簡単になって、メンテナンス性が向上する。
 開発ターゲットのソースの中に、使われないコードとして残っていた関数を開発ターゲットのソースから切り離して、単体テストのコードに移すので、開発ターゲットのソース中に、無駄なコードが残ったままになって可読性を損なうことがない。

恩恵5:メンテナンスされていないテストコードがなくなる。

 コメント化や#ifdefで無効化されたテスト用のコードはメンテナンスがされていない可能性が残る。有効化してテストして失敗しても本当に信じて良いのか不安が残る。しかし、単体テストの枠組みに移行しておけば、そのテストはテスト項目としていき続けているので、そのような不安は残らない。

恩恵6:ツールとして再利用しやすいライブラリになる。

リファクタリングして単体テストしやすくしたソースは、そのライブラリを用いてテストという名の補助的なツールを使いやすくなってきている。

恩恵7:テスト用の環境で便利なライブラリを使える

実装用の環境では使えないライブラリ(開発方針による理由も含む)があっても、単体テストの中では使うことが可能です。実績が豊富なライブラリと動作の比較を行うことも、system関数の中で、便利なコマンドを用いることも可能です。どのようなライブラリを使うのかはあなたしだいです。

付記:
 CppUnitの枠組みに単体テストを増やしていくのは必ずしも容易ではありません。全ての項目をテストしきろうとは欲張らずに、テストしておかないと影響の大きい項目についてだけテストしようと考えたほうがよいと思います。
 項目を増やしてテストしていこうとすると、たくさんのライブラリを必要として単体テストのプロジェクトのビルド自体を通すために四苦八苦する場合もあるかと思います。
 いい具合に手を抜きながらやってみるのがいいと思います。

恩恵:動作の詳細が不明なライブラリをとりだして、その詳細を確認できる。

 十分にドキュメントされていない仕様が不明なコードがあったときに、その詳細を実際に多くの条件で試してみることで確認することができます。未開発部分のため、動作させることのできないプログラムの中で、自分の担当分のコードの品質を確保する唯一の方法は、単体テストです。全体のシステムの中では、そのコードの実行回数が少ないために、バグが残りやすくなりがちです。「なんとなくそれっぽく動いているから」というレベルで次に進んでしまいそうです。しかし、単体テストを加えることで、本当に期待通りの動作をしていることを自信をもって進んでいくことができます。