TDDと相性の良いC、C++のユニットテスティングフレームワークとは

 これまでTDDで使えるC、C++向けのユニットテスティングフレームワークをいくつか紹介してきました。その一連の紹介の総括として、今回は、C、C++でのTDDを対象とした場合、どのようなテスティングフレームワークが望ましいのかについてまとめたいと思います。


TDD向けのテスティングフレームワークに求められる条件

 TDD向けのテスティングフレームワークに求められる条件については、以前Advent Calender向けの「TDDのはじめかた #TddAdventJp - 千里霧中」の冒頭で触れさせて頂いています。少し加筆して下記に転載します。

  • 軽快にテストを実行できる。TDDにとって、実行に1秒以上かかるテストはもう遅すぎます。そのような実害ある遅さの直接的原因になるフレームワークは避けた方が無難です。
  • 簡潔なテストコードでテストを実装・実行できる。Assertion MethodやTest Methodなどの構文が簡潔で、またテストを実行するためのコードもコンパクトに済ませられるものが推奨されます。例えばテストディスカバリ機能(テストメソッドを書いたら自動的にそれをピックアップして実行してくれる機能)はあると望ましい機能の1つです。
  • 明快かつ軽快にテスト結果を表示できる。テスト結果を通知するUIは、REDかGREENかの把握を一瞬で済ませられるように、色や配置等が工夫されていると効率が向上します。
  • 製品コードの開発環境と連携可能。TDDでは製品コードとテストコードの開発を頻繁に切り替えていくことになりますので、それらの編集領域を簡単に切り替えられるような環境が望まれます。例えばIDEに組み込み可能なフレームワークは有望な選択肢です。またIDEに組み込めなくとも、バッチファイルやスクリプトで実行できるとIDEなど開発環境と連携しやすくなります。

C、C++向けフレームワークが抱える制約

 上記は全て重要ですが、C、C++をターゲットとした場合に特に注意が必要なのが、「実行速度の軽快さ」「テストコードの簡潔さ」「(自動)テストディスカバリ機能」です。というのも、C、C++でのフレームワークではそれらに問題をもつものが少なくないためです。
 例えばC++ testといった有償フレームワークでは、高機能化の代償のためか、1回のテスト実行に何分~何十分も必要な場合が少なくありません。そうしたフレームワークは、デイリーテストといった用法ならまだしも、数秒から数分の感覚で何度もテストを実行するTDDではかなりの効率低下を招きます。
 また「テストコードの簡潔さ」についても、言語機能の限界(例えばアノテーション、属性、リフレクションといった言語機能の欠落、OOPの非サポートなど)などから問題を持つことが少なくありません。
 さらに「自動テストディスカバリ機能」については、単純にサポートしていないフレームワークが多いのが問題です。まずCについては、言語機能の限界として自動テストディスカバリ機能を実現するのが困難です(実現には外部スクリプトやツール、開発環境の支援が必要になります)。C++についても、CppUnitのように開発環境の制約や初期設計のまずさから搭載していないものがいくつかあります


C、C++でのTDD向けフレームワーク

 以上を踏まえてですが、C、C++でTDDを行う場合に適していると思うフレームワークをまとめます。

C++

 まずC++については、Google Test、CppUTestが有力です。
 これらはテストコードが簡潔で、自動テストディスカバリ機能を搭載しており、テストも軽快で出力も見やすいです。またMockやパラメータ化テストなど現代的な機能のサポートにも積極的です。また、Googleでは恐らく普通にTDDをやっているでしょうし、CppUTestはTDDの世界的な推進者が開発に関わっていることもあって、TDDとの親和性も高いです。
 なお2つのフレームワークの選択基準として、Google Testは日本語資料が充実している点、CppUTestはマルチプラットフォーム対応が進んでいる点が挙げられます。

C

 次にCについては、Unity、CppUTestが有力だと思います。繰り返しになってしまいますが、テストコードが簡潔で、自動テストディスカバリ機能を搭載しており、テストも軽快で出力も見やすいためです。なおUnityもTDDの世界的な推進者が開発に関わっていることもあって、TDDと親和性があります。
 ただCについては突出したフレームワークというものが存在しませんので、ほかにも妥当な選択肢が存在します。例えば用途をTDDに特化するならばPCUnitは妥当な選択肢になります。また自前でテストランナー生成ツール等を用意すれば、CSpecやCUnit等も使用に耐えるものになります。


おまけ1:CUnit、CppUnitという罠

 xUnitファミリーのC、C++代表格としてはCUnit、CppUnitが有名です。一昔前のC、C++のTDDの解説でも、しばしばサンプルコードにCUnit、CppUnitが使用されてきました。
 ただ注意として、今となってはこれらはTDDには不向きなフレームワークになっています。というのもCUnit、CppUnitは以下のような制約を抱えているためです。

  • テストディスカバリ機能を十分にサポートしていない
  • テストコード(特にテストランナー)の記述が煩雑になりがち
  • 開発が停滞しており、パラメータ化テスト・Mockライブラリのサポートといった現代的な機能が不足

 なお中でもCppUnitについては、開発者であるMichael Feathersが自著で自ら失敗作だといい、CppUnitLiteという後続プロジェクトを新たにスタートさせています。

 注意として、xUnitファミリーというと、「言語の略語」+「Unit」という命名(例えばSUnit、JUnit、CUnit等)がそれぞれの言語の代表だというイメージを持たれがちですが、そんなことはありません。少なくとも以下の点には留意が必要です。

  • CUnitもCppUnitもそれぞれの個人の開発者が好き勝手に付けた名前です。「言語や言語の略語」+「Unit」という名前だからといっても、正統な組織が規律をもって開発しているなんてことはありません。
  • CUnitやCppUnitが生まれたxUnitファミリー開発初期は、フレームワークに求められる要求が明確になっていませんでした。結果、以下のような問題を生んでいます。
    • 初期に開発されたフレームワークは、xUnitファミリーが満たすべき命名規則や構成、処理順序などの決まりごとに準拠していないことがあります。例えばNUnitはその典型です。
    • TDDやユニットテストにとって都合のよい機能や設計を細かくサポートできていないことがあります。例えばテストディスカバリ機能の非サポート、テストランナーの煩雑な記述等です。

 もちろんこれらの課題は使用者の工夫で克服できることもあります。自分も昔はCUnitに自作のテストランナー生成ツールを組み合わせることで、普通にTDDができていました。


おまけ2:自動テストディスカバリの実装

 C++の自動テストディスカバリの実現手段についてですが、これはファイル内スコープの静的オブジェクトのコンストラクタを活用しています。
 単純な例ですが、例えば以下のコードを実行するとhogeオブジェクトのコンストラクタが実行時に最初に動き、結果的に"Failure"が出力されます。さらにプリプロセッサのマクロ置換を使えば、こうした仕組みを自然なテストメソッドの記述の中に隠すことができます。

//piyo.h
#include <string>
extern std::string piyo;

#include "stdafx.h"
#include "hoge.h"

 

//hoge.cpp
#include <piyo.h>

class Hoge
{
public:
    Hoge()
    {
        piyo = "Failure"; 
    }
};

static Hoge hoge;

 

//main.cpp
#include <iostream>
#include "piyo.h"

std::string piyo;

int main()
{
    std::cout << piyo << std::endl;
    return 0;
}

 なお以前は主要な処理系でも、静的オブジェクトのコンストラクタの実行タイミングが不定で上記コードが適切に動くかわからないことがありました。それらがCppUnitといった初期のC++フレームワークの自動ディスカバリ機能の非サポートにつながっているとのことです。