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)