VC++.NETでのCppUnit

CppUnitがどうもVC7.1ではうまく動いてくれない。ユニットテストがアクセス違反で落ちてしまう。マルチスレッド版のiostreamの排他制御の中で初期化されていないミューテックスにアクセスしているのが原因らしい。おかしいな。STLのソースを見る限りでは、初期化コードはあるように見える。volatile忘れとか、それ系なんだろうか。

どうせ、ユニットテストであってリリース時に出すものじゃなし、元々VC7のSTLは好きじゃないし、思い切ってSTLportに変えてみた。今度はうまく走る。

いつもgccばっかりだったもんで、VCでxUnitは初めてだ。どういう構成が適当なのか分からないけど、次のようにしてみた。

  1. ソリューションにユニットテスト用のプロジェクトを追加。
    • テスト対象がWin32ネイティブなCOMサーバーなので、ユニットテストはWin32コンソールアプリケーションとした。
  2. 一度プロジェクトをソリューションから外して関連ファイルを移動し、メインプロジェクトと同じディレクトリに配置し直す。
  3. どうせインクルードするファイルやらマクロ定義やらは変わらないのだから、stdafx.*は共有する。ただし、微妙にコンパイラオプションが違うので、生成されるプリコンパイル済みヘッダは別ファイルにしておく。
  4. テストケースのソースファイルからテスト対象のソースファイルをインクルード。
    • このとき、Javaでいうpackage privateなアクセスが欲しいのだけど、テストのためにテスト対象に手を加えるのは何か違う気がするのでfriendにはしなかった。代わりに、テスト対象ソースをインクルードするところでだけprotectedをpublicに#defineした。
  5. ビルド後のイベントとして$(TargetPath)を指定。これでこのプロジェクトをビルドするだけでユニットテストが実行される。失敗をIDEがタスク一覧に出してくれるよう、出力形式はCompilerOutputterにしておく。
  6. CppUnitがメモリーリークを起こすが、これは終了直前で問題ない。アサーションにひっかかってabort()してしまうのは困るので、Debugモードでも非デバッグ版のランタイムとリンクするように設定する。あ、いっそのこと、デバッグモードを削除してしまってもいいのかな。

大体こんな感じになった。

MainProject.cpp

#include "stdafx.h"
#include "resource.h"

[
  module(
    dll, 
    uuid = "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX}", 
    name = "XXX", 
    resource_name = "IDR_XXX"
  )
]
class CMainProjectModule
{
public:
};
MainProjectTest.cpp

#include "StdAfx.h"
#include "MainProject.cpp"

#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>
#include <cppunit/CompilerOutputter.h>
#include <iostream>
#include <cstdlib>

int main(int argc, char* argv[])
{
  CppUnit::TextUi::TestRunner runner;
  CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry();

  runner.addTest( registry.makeTest() );
  runner.setOutputter(CppUnit::CompilerOutputter::defaultOutputter(&runner.result(), std::cout));

  bool wasSuccessful = runner.run();
  return wasSuccessful ? EXIT_SUCCESS : EXIT_FAILURE;  
}
Testee.cpp

#include "StdAfx.h"
#include "Testee.h"

[
  coclass,
  uuid("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX")
]
class CTestee
  : public ITestee
{
 // Implementation
};
TesteeTest.cpp

#include "StdAfx.h"
#include <cppunit/TestCase.h>
#include <cppunit/TestAssert.h>
#include <cppunit/extensions/HelperMacros.h>
#define protected public
#include "./Testee.cpp"
#undef protected

class CTesteeTest
  : public CPPUNIT_NS::TestCase
{
public:
  CPPUNIT_TEST_SUITE(CTesteeTest);
    CPPUNIT_TEST(testQueryInterface);
    ...
  CPPUNIT_TEST_SUITE_END();

public:
  CTesteeTest() {}

  void setUp()
  {
    // do something
  }

  void tearDown()
  {
    // do something
  }

  void testQueryInterface()
  {
    CComPtr<ITestee> pTestee;

    HRESULT hr;
    hr = m_pTestee->QueryInterface(__uuidof(Testee), reinterpret_cast<LPVOID*>(&pTestee));
    CPPUNIT_ASSERT_MESSAGE( "implements ITestee", SUCCEEDED(hr) );
ED(hr) );
  }

  ...
private:
  const CAutoPtr<CTestee> m_pTestee;
};

CPPUNIT_TEST_SUITE_REGISTRATION(CTesteeTest);

ウェブで資料を漁っていたら、2chのスレで、テストメソッドを自動抽出できないのは格好悪いという意見を発見。確かに、たいした手間ではないんだけど、登録を意識させられるかさせられないかというのは結構大きい。

それから、テストケースクラスは再帰テンプレートパターンで自動登録の方が良くないだろうか。LokiのPrototype登録みたいに。CPPUNIT_TEST_SUITE_REGISTRATION()マクロは確かに近いことをやってるんだけど。

テストケースは自動登録させて、その中のテストメソッドはPEを解析して無理矢理抽出とか。あるいはVCの機能で何とかするとか。

いずれにしても、IDE内で機能はそろってるはずなのに自動抽出ができないのはいまいちだと思う。調べたけど河童は少し違うしねぇ。特に、Win32環境ではPerlの要求は……。私のマシンには入ってても、他の環境でそのためだけにPerlは入れられないし。今やってる個人プロジェクトが終わったら次はこれに着手しようかな、と思う。