株式会社エス・スリー・フォー

TestRunner自動生成 (for CppUnit 1.6.2)

単体テストフレームワーク(UnitTestFramework): xUnit の解説書:
「eXtreme Programming テスト技法」
翔泳社 ISBN4-7981-0128-1

では、C++版xUnitのひとつ CppUnit-xを利用したUnitTestの手順について解説しています。

C++のテストをCppUnitで書くとき、最も面倒なのはテストスイートの構築です。 この本にはCppUnit-x によるテストケースを記述したヘッダを基にテストスイートを生成し、 そして実行するC++コードを吐くruby スクリプト ‘TestRunnerFactory.rb’ が紹介されています。

CppUnit-x 用のTestRunnerFactory.rb を本家 CppUnit 1.6.2で使えるように書き換えたものを以下に示します。

TestRunnerFactory.rb (for CppUnit 1.6.2)
class TestSuiteFactory

  def initialize(classname)
    @classname = classname
    @testMethods = []
  end

  def makeAddSuite
    "  suite.addTest(" + @classname + "_suite());\n"
  end

  def makeSuiteMethod(file)
    file.puts "#include \"" + @classname + ".h\""
    file.puts "CppUnit::Test* " + @classname + "_suite() {"
    file.puts "  typedef CppUnit::TestCaller<" + @classname + "> caller_type;"
    file.puts "  CppUnit::TestSuite* suite = new CppUnit::TestSuite(\"" + @classname + "\");\n"
    @testMethods.each{ |method|
       file.print "  suite->addTest(new caller_type(\"" + method + "\", " 
       file.print @classname + "::" + method + "));\n"
    }
    file.puts "  return suite;"
    file.puts "}"
  end

  def parse
    lex = /.*void (test.*)\(\).*/
    File.readlines(@classname + ".h").each do |f|
      if lex =~ f then
        @testMethods.push($1)
      end
    end
  end

end

class TestRunnerFactory

  def initialize
    @tests = []
  end

  def getTestList
    dir = Dir.open(Dir.pwd)
    reg = /(.*Test)\.h/
    dir.each { |file|
      if reg =~ file then
        @tests.push TestSuiteFactory.new($1)
      end
    }
    dir.close
  end

  def makeRunner
    tester = File.new("Tester.cpp", "w")
    tester.puts "#include <iostream>"
    tester.puts "#include <cppunit/TestCaller.h>"
    tester.puts "#include <cppunit/TestSuite.h>"
    tester.puts "#include <cppunit/TextTestResult.h>"
    @tests.each { |aClass|
      aClass.parse
      aClass.makeSuiteMethod(tester)
    }
    tester.puts "int main() {"
    tester.puts "  CppUnit::TestSuite suite;"
    tester.puts "  CppUnit::TextTestResult result;"
    @tests.each { |aClass|
      tester.puts aClass.makeAddSuite
    }
    tester.puts "  suite.run(&result);"
    tester.puts "  std::cout << result << std::endl;"
    tester.puts "  return 0;"
    tester.puts "}"
    tester.close
  end

end

factory = TestRunnerFactory.new
factory.getTestList
factory.makeRunner

これを使えばテスト効率がぐんと向上します。が、TestRunnerFactory.rbはrubyスクリプトですから、もちろんrubyなしには実行できません。

そこで、このスクリプトが行う処理をそのままC++で実現しました。Microsoft Visual C++ 6.0 でコンパイル/実行を確認しました。

なお、このアプリケーションの実装には Boost 1.26.0 の正規表現ライブラリ ‘Regex++’を利用しました。

TestRunnerFactory.cpp
/*
 * TestRunnerFactory.cpp
 * compiled & tested on Visual C++ 6.0 & Boost 1.26.0 Regex++
 *
 * [build]
 * cl -GX -MD -I[BOOST_ROOT] TestRunnerFactory.cpp setargv.obj 
 *    -link -libpath:[BOOST_ROOT]/libs/regex/build/vc6
 *
 * [usage]
 * TestRunnerFactory <file...>
 *   - parse only *Test.h
 *   - generated code goes to 'stdout'
 */

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <boost/regex.hpp>

using namespace std;
using namespace boost;

int main(int argc, char* argv[]) {

  vector<string> classnames;
  vector<string>::iterator classname;

  reg_expression<char> regex;
  match_results<const char*> results;

  /* argv[] から "xxxTest.h" を探し、classnames に追加 */
  regex = "(.*Test)\\.h";
  for ( int i= 1; i < argc; ++i ) {
    if ( regex_match(argv[i], results, regex) ) {
      classnames.push_back(results.str(1));
    }
  }

  /* お決まりの #include ... */
  cout << "#include <iostream>\n\n"
          "#include <cppunit/TestCaller.h>\n"
          "#include <cppunit/TestSuite.h>\n"
          "#include <cppunit/TextTestResult.h>\n\n";

  /* 抽出した "xxxTest.h" を #include */
  for ( classname = classnames.begin(); classname != classnames.end(); ++classname ) {
    cout << "#include \"" << *classname << ".h\"\n";
  }

  /* int main() 冒頭部 */
  cout << "\nint main() {\n"
          "  CppUnit::TestSuite suite;\n"
          "  CppUnit::TestSuite* group;\n"
          "  CppUnit::TextTestResult result;\n\n";

  /* "xxxTest.h" から "void testXXXX()" を探し、TestSuiteに追加 */
  regex = "[ \t]void[ \t]+(test.*)\\(\\)";
  for ( classname = classnames.begin(); classname != classnames.end(); ++classname ) {
    cout << "  group = new CppUnit::TestSuite(\"" << *classname << "\");\n";
    ifstream file((*classname + ".h").c_str());
    string line;
    while ( getline(file, line) ) {
      if ( regex_search(line.c_str(), results, regex) ) {
        cout << "  group->addTest(new CppUnit::TestCaller<" << *classname << ">(\"" 
             << results.str(1) << "\", " << *classname 
             << "::" << results.str(1) << "));\n";
      }
    }
    cout << "  suite.addTest(group);\n\n";
  }

  /* 実行と結果の出力 */
  cout << "  suite.run(&result);\n"
          "  result.print(std::cout);\n\n"
          "  return 0;\n"
          "}" << endl;
  return 0;
}

早速使ってみましょう。

非常に簡単なクラスCounter、およびCounterのテストコードを用意しました。

テストされるオブジェクト Counter

Counter.h
#ifndef __COUNTER_H__
#define __COUNTER_H__

class Counter {
public:
  Counter();
 ~Counter();
  int value() const;
  void incr();
  void decr();
  void clear();

private:
  int value_;
};
#endif
Counter.cpp
#include "Counter.h"

Counter::Counter() : value_(0) {
}

Counter::~Counter() {}

int Counter::value() const {
  return value_;
}

void Counter::incr() {
  ++value_;
}

void Counter::decr() {
  --value_;
}

void Counter::clear() {
  value_ = 0;
}

Counterのテストコード

CounterTest.h
#ifndef __COUNTERTEST_H__
#define __COUNTERTEST_H__

#include <cppunit/TestCase.h>
#include "Counter.h"

class CounterTest : public CppUnit::TestCase {
public:
  void test_init();
  void test_incr();
  void test_decr();
  void test_clear();

  virtual void setUp();
  virtual void tearDown();

};

#endif
CounterTest.cpp
#include "CounterTest.h"

void CounterTest::setUp() {
  // 前準備
}

void CounterTest::tearDown() {
  // 後始末
}

////////////// TEST CASES //////////////////

void CounterTest::test_init() {
  Counter c;
  CPPUNIT_ASSERT_EQUAL( 0, c.value() );
}

void CounterTest::test_incr() {
  Counter c;
  CPPUNIT_ASSERT_EQUAL( 0, c.value() );
  c.incr();
  CPPUNIT_ASSERT_EQUAL( 1, c.value() );
  c.incr();
  CPPUNIT_ASSERT_EQUAL( 2, c.value() );
}

void CounterTest::test_decr() {
  Counter c;
  CPPUNIT_ASSERT_EQUAL( 0, c.value() );
  c.decr();
  CPPUNIT_ASSERT_EQUAL( -1, c.value() );
  c.decr();
  CPPUNIT_ASSERT_EQUAL( -2, c.value() );
}

void CounterTest::test_clear() {
  Counter c;
  CPPUNIT_ASSERT_EQUAL( 0, c.value() );
  c.incr();
  CPPUNIT_ASSERT_EQUAL( 1, c.value() );
  c.clear();
  CPPUNIT_ASSERT_EQUAL( 0, c.value() );
}

TestRunnerFactory CounterTest.hによって、以下のようなコードが生成されます。

TestRunnerFactoryが生成したコード

Tester.cpp
#include <iostream>

#include <cppunit/TestCaller.h>
#include <cppunit/TestSuite.h>
#include <cppunit/TextTestResult.h>

#include "CounterTest.h"

int main() {
  CppUnit::TestSuite suite;
  CppUnit::TestSuite* group;
  CppUnit::TextTestResult result;

  group = new CppUnit::TestSuite("CounterTest");
  group->addTest(new CppUnit::TestCaller<CounterTest>("test_init", CounterTest::test_init));
  group->addTest(new CppUnit::TestCaller<CounterTest>("test_incr", CounterTest::test_incr));
  group->addTest(new CppUnit::TestCaller<CounterTest>("test_decr", CounterTest::test_decr));
  group->addTest(new CppUnit::TestCaller<CounterTest>("test_clear", CounterTest::test_clear));
  suite.addTest(group);

  suite.run(&result);
  result.print(std::cout);

  return 0;
}

生成された Tester.cpp、テストコード CounterTest.cpp、テスト対象Counter.cpp をコンパイルし、 CppUnitライブラリをリンクすればテストモジュールの完成です。

% TestRunnerFactory CounterTest.h > Tester.cpp

% cl -GX -MD -Id:/cppunit-1.6.2/include Tester.cpp Counter.cpp CounterTest.cpp cppunit.lib
     -link -libpath:d:/cppunit-1.6.2/lib

Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8804 for 80x86
Copyright (C) Microsoft Corp 1984-1998. All rights reserved.

Tester.cpp
Counter.cpp
CounterTest.cpp
コードを生成中...
Microsoft (R) Incremental Linker Version 6.00.8447
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/out:Tester.exe
-libpath:d:/cppunit-1.6.2/lib
Tester.obj
Counter.obj
CounterTest.obj
cppunit.lib

% Tester
....
OK (4 tests)