クリアコードはフリーソフトウェア開発で培った技術力を提供しています。特にMozilla製品(Mozilla FirefoxとMozilla Thunderbird)とRubyに関連した開発を得意としています。
先日、Ruby用のxUnit系テスティングフレームワークtest-unit 2.0.7とC・C++用のxUnit系テスティングフレームワークCutter 1.1.1がリリースされました。
どちらも、テストの書きやすさ(テストをキレイに書けるとテストを保守しやすい)だけではなく、テストが失敗した時に「できるだけ素早く問題の原因にたどり着ける」ことも重視しています。
Rails/Rack界隈ではCucumberやWebrat、capybaraなどを使って、「"ログイン"ボタンをクリックする」とか「click_link("ログイン")」などと、直感的にテストを書けるようになっています。では、テストが失敗したときの結果はどのように表示されるでしょうか。HTML全体やテキスト全体が表示されて、「"ログイン"というボタン(リンク)はなかったよ」と言われたらどうでしょう。あなたのコードはどこが悪かったのでしょう。
そういうときに、失敗結果を見て、すぐに「あぁ、ここが悪いかも!」と作業を進めていけるようなテスティングフレームワークにしたいものです。開発はデバッグの連続なのですから、よりスムーズにデバッグ作業を進める手助けとなるツールを使って開発したいですよね。
test-unitやCutterはWebアプリケーション用に特別なサポート機能は提供していないので上記のようなことをうまい具合に解決できるわけではないのですが、ライブラリのテストでは上記のようなことをうまい具合に解決する機能を提供しています。
一応、リリースで変わったことを少し書いておきます。
機能面でもよくなっているのですが、インストールまわりだけにしておきます。
Cutterプロジェクトでは、これまでDebian, Ubuntu用のapt-lineとMacPortsのPortfileを提供していましたが、今回のリリースから、FedoraのYumリポジトリも提供するようにしました。以下でインストールできます。yumの管理下に入るのでアップデートも簡単ですね。
% sudo rpm -Uvh http://cutter.sourceforge.net/fedora/cutter-repository-1.0.0-0.noarch.rpm % sudo yum install cutter
test-unitの方は変わらずgemをサポートしているので、こちらもインストール・アップデートが簡単ですね。
書きやすさだけではなくデバッグしやすさも重視したテスティングフレームワークに興味のある方は使ってみてはいかがでしょうか。
注: 長いです。
スクリプト言語でのxUnit実装を使ったことがある方なら、テストを定義するだけでテストが実行されることが当たり前ではないでしょうか。c2.comのWikiによると、これはTest Collectorというそうです。定義したテストを自動的に集めてくる機能のことです。
一般的にTest Collectorの機能は言語が提供するリフレクションやメタプログラミングを使って実現されます。
例えば、Rubyのtest-unit 2.xでは、リフレクションを使う方法とメタプログラミングを使う方法の両方をサポートしています。リフレクションを使う方法ではObjectSpace.each_object(Class)ですべてのクラスを取得し、その中のTest::Unit::TestCaseのサブクラスを集めます。メタプログラミングを使う方法ではTest::Unit::TestCase.inheritedを定義して、サブクラスが定義された時のフックでそのサブクラスを集めます。
Pythonでもモジュールオブジェクトからモジュール内のオブジェクトにアクセスすることができるので、同様の方法でテストを集めることができます。JavaScriptでも、オブジェクトに定義されているプロパティを列挙することができるので、同様の方法でテストを集めることができます。(JavaScriptとXULで実装されているUxUも同様のことをしています。)
一方、CやC++ではリフレクションやクラスをファーストクラスオブジェクトとして扱えないため、自動でテストを集めるためには一工夫必要になります。一昔前のxUnitでは、定義したテストを手動で登録していました。
それでは、C/C++での一工夫の方法として以下の4つをxUnit実装と一緒に紹介します。C++用のxUnitを選択する時の参考にしてください。
最初はCppUnitのケースです。CppUnitではテストを定義するだけでは、自動でテストを集めてはくれません。しかし、便利マクロを用意して、手動でテストを登録する面倒さを減らしています。
以下はCppUnit Cookbookにあるソースコードをベースにしています。public内のテスト定義とは別にCPPUNIT_TEST_SUITEからCPPUNIT_TEST_SUITE_ENDの間でテストを登録しています。
// complex-number-test.cpp #include <cppunit/extensions/HelperMacros.h> class ComplexNumberTest : public CppUnit::TestFixture { CPPUNIT_TEST_SUITE( ComplexNumberTest ); CPPUNIT_TEST( testEquality ); CPPUNIT_TEST( testAddition ); CPPUNIT_TEST_SUITE_END(); private: Complex *m_10_1, *m_1_1, *m_11_2; public: void setUp() { m_10_1 = new Complex( 10, 1 ); m_1_1 = new Complex( 1, 1 ); m_11_2 = new Complex( 11, 2 ); } void tearDown() { delete m_10_1; delete m_1_1; delete m_11_2; } void testEquality() { CPPUNIT_ASSERT( *m_10_1 == *m_10_1 ); CPPUNIT_ASSERT( !(*m_10_1 == *m_11_2) ); } void testAddition() { CPPUNIT_ASSERT( *m_10_1 + *m_1_1 == *m_11_2 ); } }; CPPUNIT_TEST_SUITE_REGISTRATION( ComplexNumberTest );
実行する場合は以下のようなmain関数を定義する必要があります。
// main.cpp #include <cppunit/extensions/TestFactoryRegistry.h> #include <cppunit/ui/text/TestRunner.h> int main( int argc, char **argv) { CppUnit::TextUi::TestRunner runner; CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry(); runner.addTest( registry.makeTest() ); bool wasSuccessful = runner.run( "", false ); return wasSuccessful; }
上記の二つのファイル(とComplexNumberの実装)を使ってビルドします。
% g++ -o complex-number-test complex-number-test.cpp main.cpp \
-lcomplex-number -lcppunit
テストを実行するにはビルドしたバイナリを実行します。
% ./complex-number-test .. OK (2 tests)
マクロを使ってテスト登録を簡単にしている(自動化まではしていない)例としてCppUnitを紹介しました。
CPPUNIT_TEST_SUITEなどの便利マクロを使わない場合は定義したテスト名(上の例ではtestEqualityとtestAddition)以外のことも気にしなければいけなくなります。便利マクロを使うと、テスト名だけわかっていればよいので、それに比べるとだいぶテスト作成が楽になっています。
しかし、テストを定義だけして登録し忘れたということを回避することができません。また、テストケース定義とは別にmain関数も定義する必要があり、テスト以外のことにも気を配る必要があることにも注意が必要です。
C++で書かれたテストコードを直接C++コンパイラでコンパイルするのではなく、テストコードに必要なコードを追加したC++ソースコードを生成して、それをコンパイルする方法です。C++コンパイラでのビルドする前に一度変換処理を行えるので、テストコードへの記述が減ることが利点ですが、変換処理を行うのがやや面倒です。自動化されば気にならなくなるでしょう。
まずはCxxTestのケースです。CxxTestではソースコードを直接ビルドするのではなく、C++のソースコードからテスト登録処理などを付加したC++ソースコードを生成し、それをビルドします。
以下はCxxTest User Guideにあるソースコードをベースにしています。テストの定義だけでテスト登録処理は含まれていません。
// MyTestSuite.h #include <cxxtest/TestSuite.h> class MyTestSuite : public CxxTest::TestSuite { public: void testAddition( void ) { TS_ASSERT( 1 + 1 > 1 ); TS_ASSERT_EQUALS( 1 + 1, 2 ); } };
以下のようにビルドします。
% cxxtestgen --error-printer -o cxxunit-tests.cpp MyTestSuite.h % g++ -o cxxunit-tests cxxunit-tests.cpp
cxxtestgenでMyTestSuite.hにあるテスト定義にテスト登録処理などを加えてcxxunit-tests.cppを生成します。余談ですが、cxxtestgenはPythonスクリプトです。また、CxxUnitはライブラリを提供せず、ヘッダーファイルのみを提供します。
バイナリを実行するとテストが走ります。
% ./cxxunit-tests Running 1 test.OK!
テスト登録が完全に自動化されているのでCppUnitよりも新規テストの追加が容易です。テストの登録しわすれもありません。ただ、cxxtestgenとC++コンパイラで2回コンパイルする必要があることが少し手間だと言えます。
続いてQtが提供するQTestLibのケースです。QTestではQtが提供するスロットの仕組みを使って、定義されているテストを集めます。スロットがどのように定義されているかをプログラム中から扱うために、QtはC++のソースコードをプリコンパイルしますが、QTestでも同様にプリコンパイルする必要があります*1。
以下はQTestLibのチュートリアルにあるソースコードををベースにしています。
// test-qstring.cpp #include <QtTest/QTest> class TestQString: public QObject { Q_OBJECT private slots: void toUpper() { QString str = "Hello"; QCOMPARE(str.toUpper(), QString("HELLO")); } }; QTEST_MAIN(TestQString) #include "test-qstring.moc"
QTestLibでもmain関数は定義しなければいけませんが、QTEST_MAINという便利マクロが用意されています。
以下のようにビルドします。
% mkdir test-qstring % mv test-qstring.cpp test-qstring % cd test-qstring % qmake -project "QT += testlib" % qmake % make
バイナリを実行するとテストが走ります。
% ./test-qstring ********* Start testing of TestQString ********* Config: Using QTest library 4.5.3, Qt 4.5.3 PASS : TestQString::initTestCase() PASS : TestQString::toUpper() PASS : TestQString::cleanupTestCase() Totals: 3 passed, 0 failed, 0 skipped ********* Finished testing of TestQString *********
このようにQTestLibではテスト登録のために必要なコードはQTEST_MAINでクラスを指定している部分だけです。個々のテストは指定する必要がありません。
メタオブジェクト情報を生成すること、また、それを読み込んでいる#include "test-qstring.moc"のところはQTestLib独自のことではなく、Qt全般のことなので、Qtを利用している場合は追加で必要な作業とはいえないでしょう。つまり、QTestLibのテストを集める方法は完全には自動化されていませんが、Qt開発者にはそれほど負担もかからず自然に書けるようになっている使いやすいAPIといえます。一方、Qt開発者でない場合は、面倒に見えるでしょう。
CppUnitでもマクロでテストを登録していますが、それをもう一歩進めたのがこの方法です。CppUnitでは、テスト定義は通常の関数定義でしたが、この方法ではそこでマクロを使い、テスト定義と同時にテストを登録します。
まずは、Google Testです。Google Testではテスト定義時にTESTマクロを使います。以下はGoogleTestSamplesにあるソースコードをベースにしています。
// test-factorial.cpp #include <gtest/gtest.h> int Factorial(int n) { int result = 1; for (int i = 1; i <= n; i++) { result *= i; } return result; } TEST(FactorialTest, Negative) { EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_TRUE(Factorial(-10) > 0); } int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }
プリコンパイル方式でもテストの登録忘れはありませんが、この方法でも登録を忘れることがありません。テスト定義の方法が通常の関数定義とは異なる書式になることに慣れることができるのであれば、この方式で負担が少なくテストを書けるようになるでしょう。
以下のようにビルドします。
% g++ -o test-factorial test-factorial.cpp -lgtest
バイナリを実行するとテストが走ります。
% ./test-factorial [==========] Running 1 test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from FactorialTest [ RUN ] FactorialTest.1 [ OK ] FactorialTest.1 (0 ms) [----------] 1 test from FactorialTest (0 ms total) [----------] Global test environment tear-down [==========] 1 test from 1 test case ran. (0 ms total) [ PASSED ] 1 test.
上記の例では触れていませんが、フィクスチャ(setup/teardown)を使う場合は、テストクラス名を揃える必要があるなど、同じグループのテストを作る場合は重複する部分がでてしまいます。例えば、QTestLibのようにクラス内にテストを定義する方法では以下のようになります。
class MyTest { void setup() {...} void teardown() {...} void test1() {...} void test2() {...} void test3() {...} }
一方、Google Testの場合は、スコープが使えず、以下のようにクラス名を複数回書く必要があります。
class FooTest : public testing::Test { protected: virtual void SetUp() { b_.AddElement(3); } Foo a_; Foo b_; }; TEST_F(FooTest, InitializesCorrectly) { EXPECT_TRUE(a_.StatusIsOK()); } TEST_F(FooTest, ReturnsElementCountCorrectly) { EXPECT_EQ(0, a_.size()); EXPECT_EQ(1, b_.size()); }
マクロを使っている場合は、間違ったテストクラス名を指定するなどコンパイルエラーになったときに意味の分からないエラーメッセージを目にすることがあるというのも注意しなければいけないポイントです。エラーメッセージを使えないと問題を発見することが難しくなります。
次に、Boost Test Libraryです。やり方はGoogle Testとだいたい同じで、Boost Test LibraryではBOOST_AUTO_TEST_CASEを使います。
// test-add.cpp #define BOOST_TEST_DYN_LINK #define BOOST_TEST_MODULE AddTest #include <boost/test/unit_test.hpp> int add( int i, int j ) { return i+j; } BOOST_AUTO_TEST_CASE( add_test ) { BOOST_CHECK_EQUAL( add( 2,3 ), 5); }
最初にBOOST_TEST_DYN_LINKとBOOST_TEST_MODULEを定義しておくと、Boost Test Libraryではmain関数を定義する必要はありません。
以下のようにビルドします。
% g++ -o test-add test-add.cpp -lboost_unit_test_framework
バイナリを実行するとテストが走ります。
% ./test-add Running 1 test case... *** No errors detected
Google Testと同じくマクロが気にならない場合やBoostに慣れている場合はテストが書きやすいでしょう。
マクロを利用する方法は言語の構文を工夫してテストを集めています。プリコンパイル方式では言語の構文はそのままで、コンパイル前に付加情報を加えることでテストを集めています。
一方、最後の共有ライブラリから探す方法ではコンパイル後の共有ライブラリから情報を取得してテストを集めます。この方式では、テストを共有ライブラリとして作成し、テスト実行用のコマンドからその共有ライブラリを読み込み、テストを実行します。こうすることにより、テスト側にテスト登録処理を埋め込む必要がなくなります*2。共有ライブラリの中からテストを見つける処理はテスト実行コマンドが頑張るからです。
最初はWinUnitです。やり方は共有ライブラリからテストを集める方式なのですが、書き方はマクロを使う方式です。テストを定義するときはBEGIN_TESTとEND_TESTで囲みます。
#include "WinUnit.h" BEGIN_TEST(AddTest) { WIN_ASSERT_TRUE(3 == add(1, 2)); } END_TEST
すでにGoogle TestやBoost Test Libraryで見たように、この使い方であれば、共有ライブラリにする必要はありません。マクロの中で一工夫することでテストの自動登録を実現できるからです。
WinUnitの利点はVisual C++で使いやすいことでしょう。マクロを使ったAPIが気にならないVisual C++開発者には有力な選択肢です。
最後はCutterです。CutterはC言語用の単体テストフレームワークとして開発されていましたが、先日リリースされた1.1.0で大きくC++対応を強化しています。
CutterではWinUnitとは違いマクロを利用しません。通常通り関数を定義するとテストとして認識されます。ただし、すべての関数がテストとして認識されるのではなく、test_からはじまる名前の関数だけをテストとして認識します。
#include <cppcutter.h> namespace calc { void test_add() { cppcut_assert_equal(5, add(2, 3)); } }
マクロを利用してテストを自動登録する方式では、フィクスチャ定義時に名前を揃える必要がありましたが、Cutterでは以下のようにnamespace内にsetup()/teardown()を定義するだけです。namespaceでグループ化されたテスト全体でフィクスチャを共有します。
#include <cppcutter.h> namespace calc { void setup() {...} void teardown() {...} void test_add() // calc::setup()/calc::teardown()が呼び出される { cppcut_assert_equal(5, add(2, 3)); } void test_sub() // calc::setup()/calc::teardown()が呼び出される { cppcut_assert_equal(5, add(8, 3)); } }
この方式では通常のC++プログラムと同様にテストを書くことができるため、新しくテストを書くことの敷居が低くなります。しかし、tes_などタイプミスをしてしまった場合に、どうしてテストが実行されないのかに気づきにくいという問題点があります。
テストも通常のプログラムと同様に開発したい場合はマクロを使わないこの方式がオススメです。
C++用の各種xUnitでのテストの書き方を、方式毎に分類して紹介しました。どんなバックグラウンドを持っているかにより、選びやすいxUnitは変わるでしょう。Visual C++開発者であればWinUnitを選ぶことが多いでしょうし、Qt開発者であればQtTestLibを選ぶことが多いでしょう。しかし、バックグラウンドから選ぶだけではなく、テストの書きやすさも判断材料に加えてみてはいかがでしょうか。
継続して開発すればそれに伴ってテストも増えていきます。しかし、テストは面倒くさがって飛ばしてしまいがちです。新しくテストを書く敷居が下がれば、テストを面倒くさがることが少なくなり、安心して開発を続けていくための土台を固めることができます。新しくテストを書く敷居を下げることは開発を継続するのであれば割に合うということです。
今回は「新しいテストの書きやすさ」を軸に様々なxUnitのやり方を紹介しました。C++用xUnitを選択する時の参考にしてみてください。
念のため書きますが、オススメはCutterです。
本日、C言語用単体テストフレームワークであるCutterの新バージョン1.0.8が肉リリースされました。
先日の2つの記事(その1、その2)でも紹介しましたが、1.0.8の重要な新機能はHTTPインターフェースのテスト機能 SoupCutter です。SoupCutter では GNOMEプロジェクトで開発されている HTTPサーバー・クライアントライブラリの libsoup をバックエンドに利用しており、HTTPサーバーやクライアントプログラムのテストを簡単に記述できるようになっています。
SoupCutter の使い方は、前回の紹介記事に詳しく書いてあるので是非これを参考に使ってみてください。この記事では SoupCutterの使用例として groonga のHTTPインターフェースのテストを作成しているのですが、SoupCutter を使った HTTP インターフェースのテストは実際に groonga 本体にも取り込まれています。
また、1.0.8からFedoraのrpmパッケージや、Mac OS Xのportsパッケージ(MacPorts)、Debian、Ubuntuのdebパッケージもサポートするようになったので、これまでよりも手軽に Cutter を導入できるようになりました。
汎用的なHTTPのライブラリを使ったとしても、C言語でHTTPインターフェースのテストを開発しようと思うと一手間かかってしまうのではないかと思います。しかし、HTTPのテストに特化したSoupCutterを利用すれば簡潔にテストを記述できる上に、その気になれば libsoup の豊富な機能をフル活用することもできるようになります。
ますます便利になった Cutter を使って、皆さんが関わっているプロジェクトのテストを作成してみませんか?
前回に引き続き、クリアコードインターン記事の2回目です。前回の記事で紹介したCutterのHTTPテストモジュールであるSoupCutterを使って、全文検索エンジンgroongaのHTTPインターフェースのテストを作成したので、今回はその紹介をしたいと思います。SoupCutterが実際どのように使えるかという実例として、よい題材なのではないかと思います。
全文検索エンジンgroongaはHTTPサーバー機能を備えており、Webブラウザからアクセスすることでテーブルを作成したり、データベースの中身を調べたりすることができます。このようにブラウザからデータベースを管理するために、groongaではデータベースを操作するための基本的なAPIをHTTPリクエストによって呼び出せるようにしています。例えば、localhost:10041 で groonga のサーバーを実行しているときに、http://localhost:10041/table_list を GET すると、テーブルの一覧を取得することができます。
それでは、Cutter でどのようにしてテストを開発していくかを見ていきましょう。
今回は groonga のHTTPサーバー機能のテストを行うため、まずは groonga でHTTPサーバーを走らせなければなりません。Cutterには外部コマンドを簡単に扱うことができる GCutEgg というオブジェクトがあります。groonga でポート 4545 を listen する HTTPサーバーを起動するには、以下のようなコマンドを実行します。
groonga -s -p 4545 -n /path/to/dbfile
このコマンドをテストのプログラムから実行しなければなりません。GCutEgg を使うと、以下のように簡単にコマンドを実行することができます。
GCutEgg *egg = gcut_egg_new("groonga", "-s", "-p", "4545", "-n", "/path/to/dbfile", NULL); gcut_egg_hatch(egg, NULL);
たったこれだけで、簡単に groonga のHTTPサーバーを準備することができました。このサーバーはテストの間は実行していて、テストが終わるごとに終了してほしいので、setup で実行を始めて、tear down で終了してあげればよいでしょう。
また、前回の記事で紹介した Cutter のHTTPクライアント SoupCutClient も setup で準備しておくとよいでしょう。
client = soupcut_client_new(); soupcut_client_set_base(client, "http://localhost:4545/");
soupcut_client_set_base で SoupCutClient にベースURIを設定しておくことで、実際にGETリクエストを送信するときのURI指定で楽をすることができます。SoupCutter では、soupcut_client_get(client, "http://localhost:4545/path/to/something") のようにGETリクエストを送ることができるのですが、ベースURIを設定ておけば soupcut_client_get(client, "/path/to/something") と書くだけで、 http://localhost:4545/path/to/something にGETリクエストを送ることができるようになります。
さらにもうひとつ。GCutEgg と SoupCutClient はどちらも GLib のオブジェクトとして実装されており、解放するときは g_object_unref を呼ぶだけでデストラクタが呼ばれ、適切にオブジェクトを解放してくれます。Cutter では、オブジェクトを破棄する関数と共にオブジェクトを登録しておくと、テストの tear down 時に自動でオブジェクトを解放してくれるという便利機能があります。どうやるかというと、下記のようにするだけです。
cut_take(client, g_object_unref); cut_take(egg, g_object_unref);
またこれらは、GLibをサポートしたGCutterの関数を使うと、
gcut_take_object(G_OBJECT(client)); gcut_take_object(G_OBJECT(egg));
と書くこともできます。 これで client と egg は自動的に tear down 時に解放されるようになります。Cutterでは適切な下準備をしておくと、tear down 用の関数でわざわざ後片付けをしなくてもOKです。便利ですね。
ここまでをまとめると、テストのセットアップは次のように書くことができます。
static GCutEgg *egg; static SoupCutClient *client; void cut_setup(void) { client = soupcut_client_new(); soupcut_client_set_base(client, "http://localhost:4545/"); gcut_take_object(G_OBJECT(client)); egg = gcut_egg_new("groonga", "-s", "-p", "4545", "-n", "/tmp/http.db", NULL); gcut_egg_hatch(egg, NULL); gcut_take_object(G_OBJECT(egg)); g_usleep(G_USEC_PER_SEC); /* groonga の listen が完了するまで適当な時間待つ */ }
まずは簡単なところからテストしていきましょう。groongaのHTTPサーバーはルートにGETリクエストを送ると、本文無しで 200 STATUS OK を返してくるのでこれをテストしてみます。
void test_get_root(void) { soupcut_client_get(client, "/", NULL); /* http://localhost:4545/ を GET */ soupcut_client_assert_response(client); /* status code は 2XX かチェック */ soupcut_client_assert_equal_content_type("text/javascript", client); /* Content-Type をチェック */ soupcut_client_assert_equal_body("", client); /* 本文が空かをチェック */ }
このように、非常に簡潔にテストを書くことができます*1。
もう1つテストを書いてみましょう。groongaのHTTPサーバーは、/status にリクエストを送ると、{"starttime":1251190614,"uptime":39} というようにサーバーが開始した時刻とuptime をJSON形式でレスポンスとして応答します。starttimeもuptimeも開始した時刻や現在時刻によって刻々と変化するため、単純に assert_equal_body で期待した文字列と一致するかどうかを調べるには無理があります。このような要求に答えるために、SoupCutterでは正規表現に本文がマッチするかをテストできる soupcut_client_assert_match_body という関数を提供しています。
void test_get_status(void) { soupcut_client_get(client, "/status", NULL); soupcut_client_assert_response(client); soupcut_client_assert_equal_content_type("text/javascript", client); soupcut_client_assert_match_body("{\"starttime\":\\d+,\"uptime\":\\d+}", client); }
soupcut_client_assert_match_body を利用すると、このようにして /status をGETしたときのテストを実装することができます。
このように、柔軟なテストも簡単に作成できるのが Cutter の特徴であり、開発方針でもあります。
その他のHTTPインターフェースのテストも、groongaの側でテーブルを作っておいたりカラムを作っておいたりというコードを書かなければならないことを除けば、ほとんど上記の2つのテストと同様に開発してゆくことができます。テーブルを作成する API は、/table_create にクエリーパラメータとしてテーブル作成に必要な情報を渡すことで呼び出すことができますが、これも SoupCutter では次のように簡潔に書くことができます。
soupcut_client_get(client, "/table_create", "name", "newtable1", "flags", flags, "key_type", "Int8", "value_type", "Object", "default_tokenizer", "", NULL);
今回はテストを開発する実例を通して、SoupCutter の使い方について紹介しました。SoupCutter を使って開発された groonga のテストは、実際に groonga のレポジトリにも取り込まれています。
SoupCutter を含めた Cutter は今週中にリリース予定なので、是非みなさん使ってみてください。
*1 関数名がやや長いのは御愛嬌ということで ;-)
現在クリアコードでインターン中のはやみずです。クリアコードのインターンシップ制度は今年度から始まり、最初のインターン生として2週間クリアコードで働かせていただくことになりました。今回と次回の2つの記事で、現在インターンシップで取り組んでいる内容について紹介したいと思います。今回の記事では、C言語用単体テストフレームワークCutterへのHTTPテスト機能追加について紹介します。
ククログでも度々紹介されているCutterですが、このテストフレームワークを利用することでC言語での単体テストを非常に効率良く開発することができます。
Cutterはできるだけ簡潔にテストを記述できるように、基本的な assert 系関数以外にも様々なユーティリティ関数を提供しています。また、GLibを利用したプログラムのテストを開発するためのGCutterや、gdk-pixbuf(C言語で画像を扱うためのライブラリ)用の GdkCutter Pixbufなどのモジュールが含まれています。これらを利用することで、GLib や Gdk Pixbuf を使ったプログラムはよりこれらのライブラリに特化したテストを簡単に書くことが可能となっています。これからも対応ライブラリは増えてゆくのかも?
Cutterの強みの1つに、C言語でありながらメモリ管理の手間が非常に少ないということが挙げられます。ほとんどのテストフレームワークは set up(準備)→test実行→tear down(後片付け) という処理の流れを基本としているので、テスト中に利用するオブジェクトは tear down のときに解放してやればよいことがわかっています。Cutter は「このオブジェクトは tear down 時に解放しといてね」ということを Cutter に教えるための API を提供しているため、この API を利用することで解放忘れによるメモリリークを防ぐことができます。例えば文字列であれば、cut_take_string(const gchar *string) を利用することで文字列を動的にアロケートした領域にコピーしてそのポインタを得ることができ、tear down時にはこの文字列が自動的に解放されます。
今回開発しているCutterのHTTPテスト機能も、このパターンを使ってオブジェクトを簡単に生成して、しかも勝手に解放してくれるようになっています。
さて、本題のHTTPテスト機能を実装した SoupCutter に話を移しましょう。今回の開発では、HTTPの機能を簡単に実装するために libsoup というライブラリを利用しました。というよりも、GLibをサポートするのが GCutter、Gdk Pixbuf をサポートする GdkCutter Pixbuf などのように、libsoup をサポートする SoupCutter という位置付けのほうが正確です。しかし、簡単なテストであれば libsoup 自体には一切触れることなく作成することができるので、Cutter で HTTP サーバーや HTTP クライアントのテストを簡単にできるようにするためのモジュールだと思っていただいても大丈夫です。
SoupCutter を使って HTTP サーバープログラムが正しくレスポンスを返しているかをテストするには、次のように書くことができます。
SoupCutClient *client = soupcut_client_new(); /* http://localhost:8080/?key=value に HTTP Request を送信 */ soupcut_client_get(client, "http://localhost:8080/", "key", "value", NULL); soupcut_client_assert_response(client); soupcut_client_assert_equal_content_type("text/plain", client); soupcut_client_assert_equal_body("Hello, world", client);
SoupCutClient というのは、サーバーとやりとりしたHTTPリクエスト/レスポンスを内包しているオブジェクトです。..._assert_response では、最後に受け取ったレスポンスが 2XX (200 OK など) であるかをチェックしています。同様に、..._assert_content_type では Content-Type が text/plain であることを、..._assert_equal_body ではレスポンスの本文が Hello, world であることをチェックしています。このようにして、SoupCutter を使うと非常に簡潔な記述で HTTP サーバーが思った通りに動いているかを調べることができるようになっています。
また、SoupCutter の最初の機能としては HTTP サーバーのテスト、つまりHTTPクライアントとしての機能を実装しているのですが、このHTTPクライアント機能が正しく動作しているかをテストしなければなりません。HTTPクライアント機能をテストするためには、HTTPサーバが必要です。HTTPサーバーをテストするためにHTTPクライアントを実装し、そのHTTPクライアントをテストするためにHTTPサーバーを実装する。ややこしいですね。
というわけで、SoupCutter は簡単にHTTPサーバーを作ることもできます。
static void server_callback (SoupServer *server, SoupMessage *msg, const gchar *path, GHashTable *query, SoupClientContext *client, gpointer user_data) { .... /* リクエストを処理して結果を返す */ } SoupServer *server; server = soupcut_server_take_new(NULL); soup_server_add_handler(server, "/", server_callback, NULL, NULL); soup_server_run(server);
HTTPサーバーの作成自体はたった3行でできてしまいました。server_callback は実際にリクエストを処理してレスポンスを生成するコールバック関数です。これを SoupServer のリクエストハンドラに追加して、soup_server_run でメインループに入り、サーバーが動き始めます。
ここで注目してほしいのは、サーバーを生成するときの soupcut_server_take_new です。Cutterでは take と名前のつく関数で生成したオブジェクトは、tear down時に自動で解放されます。HTTPサーバーの場合は、ちゃんとソケットの後処理まで行い、オブジェクトを解放してくれます。つまり、HTTPサーバーを簡単に作れるだけではなく、勝手に後片付けまでしてくれます。
今回は、現在開発中である Cutter の HTTPテストモジュール SoupCutter について簡単に紹介しました。SoupCutterを使うと簡単便利にHTTPサーバー/クライアントのテストを作成することができるようになります。SoupCutter は現在クリアコードのインターンシップで開発していて、来週末に SoupCutter を含めた Cutter をリリースすることを目標に頑張っています。もし HTTP のテストを作る必要に迫られた場合には、SoupCutter を検討してみてください。
ELFから公開されている関数名を抜き出す、Mach-Oから公開されている関数名を抜き出すのPE(Portable Executable)版です。PEはWindowsの.exeや.dllなどで利用されているファイルフォーマットです。
artonさんがCodeZineでDbgHelpを利用してDLLがエクスポートしている関数を列挙するという同様の内容の記事を書いています。PEのフォーマットについても説明しているので、まず、この記事を読んでおくとよいでしょう。
ここでは、DbgHelpなどライブラリを一切使わずに自力でPEをパースし、関数名を抜き出します。そのため、MinGWでクロスコンパイルすることも簡単です。実際、CutterはMinGWを用いたクロスコンパイルに対応しています。
簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。
ファイルの内容を読み込むために、便利なGLibのg_file_get_contents()を使いたいところですが、Windows環境ではGLibがインストールされていないことが多いので、ここでは自力で読み込むことにします。
char *content = NULL; FILE *file; char buffer[4096]; size_t bytes, total_bytes = 0; file = fopen(argv[1], "rb"); if (!file) { perror("failed to fopen()"); return -1; } while (!feof(file)) { char *original_content; bytes = fread(buffer, 1, sizeof(buffer), file); total_bytes += bytes; original_content = content; content = realloc(content, total_bytes); if (!content) { free(original_content); fclose(file); perror("failed to realloc()"); return -1; } memcpy(content + total_bytes - bytes, buffer, bytes); } fclose(file);
これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。
PEのフォーマットに関する情報はwinnt.hで定義されています。winnt.hはwindows.hをincludeすると暗黙のうちにincludeされるので、windows.hだけincludeします。
#include <windows.h>
まず、ファイルがPEかどうかを判断します。
PEであればNTヘッダに"PE\0\0"という署名が入っているので、これを確認します。"PE\0\0"という署名はIMAGE_NT_SIGNATUREというマクロとして定義されているので、これを利用します。
IMAGE_DOS_HEADER *dos_header; IMAGE_NT_HEADERS *nt_headers; /* ファイルの先頭はDOSヘッダ */ dos_header = (IMAGE_DOS_HEADER *)content; /* NTヘッダを見つける */ nt_headers = (IMAGE_NT_HEADERS *)(content + dos_header->e_lfanew); /* 署名が"PE\0\0"かどうか確認 */ if (nt_headers->Signature == IMAGE_NT_SIGNATURE) { /* PEファイル */ }
PEであることが確認できたら、DLLかどうかを確認します。
if (nt_headers->FileHeader.Characteristics & IMAGE_FILE_DLL) { /* DLL */ }
公開されているシンボルはエクスポートデータセクションを見るとわかります。また、シンボルが関数かどうかは、実体がテキストセクションにあるかどうかで判断します。この方法が関数かどうかを判断する標準的な方法かはわかりませんが、実用上はこれで問題なさそうです。
よって、まず、エクスポートデータセクションヘッダとテキストセクションヘッダを見つけます。それぞれ、ヘッダの名前は以下のようになっているので、それを目印に見つけます。
以下がソースコードです。
WORD i; IMAGE_SECTION_HEADER *first_section; IMAGE_SECTION_HEADER *edata_section; IMAGE_SECTION_HEADER *text_section; /* 最初のセクションヘッダ */ first_section = IMAGE_FIRST_SECTION(nt_headers); for (i = 0; i < nt_headers->FileHeader.NumberOfSections; i++) { const char *section_name; section_name = (const char *)((first_section + i)->Name); /* 各セクションの名前を確認 */ if (strcmp(".edata", section_name) == 0) { /* エクスポートデータセクションを発見 */ edata_section = first_section + i; } else if (strcmp(".text", section_name) == 0) { /* テキストセクションを発見 */ text_section = first_section + i; } }
ヘッダが見つかったら、セクションの内容を見て、関数であるシンボル名を出力します。*1
IMAGE_EXPORT_DIRECTORY *export_directory; const char *base_address; ULONG *name_addresses; ULONG *function_addresses; DWORD min_text_section_address, max_text_section_address; /* エクスポートデータセクションの内容 */ export_directory = (IMAGE_EXPORT_DIRECTORY *)(content + edata_section->PointerToRawData); /* エクスポートデータセクション内のデータがあるアドレスを解決するための 基準になるアドレス */ base_address = content + edata_section->PointerToRawData - edata_section->VirtualAddress; /* シンボル名があるアドレス */ name_addresses = (ULONG *)(base_address + export_directory->AddressOfNames); /* シンボルの実体への相対的なアドレス(RVA)があるアドレス */ function_addresses = (ULONG *)(base_address + export_directory->AddressOfFunctions); /* テキストセクションのデータの相対的なアドレスの下限 */ min_text_section_address = text_section->VirtualAddress; /* テキストセクションのデータの相対的なアドレスの上限 */ max_text_section_address = min_text_section_address + text_section->SizeOfRawData; /* シンボル名毎に関数かどうか判断 */ for (i = 0; i < export_directory->NumberOfNames; i++) { const char *name; DWORD function_address; /* シンボル名 */ name = base_address + name_addresses[i]; /* シンボルの実体の相対的なアドレス */ function_address = function_addresses[i]; if (min_text_section_address < function_address && function_address < max_text_section_address) { /* シンボルの実体がテキストセクションにあるなら関数 */ printf("found: %s\n", name); } }
winnt.hを使って、DbgHelpなどに依存せずに、PEから公開されている関数名を抜き出す方法を紹介しました。
サンプルプログラムはDebian GNU/Linux上でMinGWを使ってクロスコンパイルし、Wineで動作を確認しました。
今のところ、Cutterがサポートしている共有ライブラリのフォーマットはELF/Mach-O/PEです。このPE編で、公開されている関数名を抜き出す方法を紹介するシリーズは最後です。もし、今後Cutterが対応するフォーマットが増えれば、そのフォーマットから関数名を抜き出す方法を紹介するかもしれません。
*1 コメント中のRVAの説明はざっくりなので、詳しくは冒頭のartonさんの記事を参照してください。
ELFから公開されている関数名を抜き出すのMach-O版です。ただし、Universal Binaryには対応していません。
Mach-Oのフォーマットの詳細はMac OS X ABI Mach-O File Format Reference(英語)を見てください。
簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。
ファイルの内容を読み込むにはGLibのg_file_get_contents()が便利です。
gchar *content;
gsize length;
g_file_get_contents(filename, &content, &length, NULL);
これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。
Mach-Oのフォーマットに関する情報はmach-o/loader.hで定義されています。また、シンボルテーブルの各エントリのフォーマットに関する情報はmach-o/nlist.hで定義されています。Mach-Oをパースするときはこれらのヘッダファイルを使うと便利です。ここでもこれらのヘッダファイルを使います。
#include <mach-o/loader.h> #include <mach-o/nlist.h>
まず、ファイルがMach-Oかどうかを判断します。
Mach-Oは最初にヘッダが入っていて、それを見ることでMach-Oかどうかを判断することができます。ここでは、32bit環境用のMach-Oだけを対象とします。64bit環境用のMach-Oを対象とする場合はコード中のいくつかの型の最後に「_64」を追加します。(例えば、mach_header -> mach_header_64。)どちらにも対応する場合はCutterのソースを参考にしてください。
struct mach_header *header; header = (struct mach_header *)content; if (header->magic == MH_MAGIC) { /* Mach-Oファイル */ }
Mach-Oであることが確認できたら、共有ライブラリかどうかを確認します。
if (header->filetype == MH_DYLIB) { /* 共有ライブラリ */ }
バンドルからもシンボルを取り出すことができるので、バンドルにも対応するのもよいでしょう。
if (header->filetype == MH_DYLIB || header->filetype == MH_BUNDLE) { /* 共有ライブラリかバンドル */ }
Mac OS X ABI Mach-O File Format Referenceの「Figure 1 Mach-O file format basic structure」にある通り、ヘッダの後にはロードコマンドと呼ばれる部分が複数続きます。いくつかあるロードコマンドのうち、興味があるのは以下の2つです。
シンボル名とシンボルが公開されているかはLC_SYMTABから取得できます。シンボルが関数と関連づけられているかは、シンボルが__TEXTセグメントの__textセクションで定義されているかどうかで判断できます。
gsize offset; uint32_t i, n_commands; uint32_t section_index = 0, text_section_index = 0; /* ファイルの先頭からコマンドの先頭までのバイト数 */ offset = sizeof(*header); /* コマンド数 */ n_commands = header->ncmds; for (i = 0; i < n_commands; i++) { struct load_command *load; load = (struct load_command *)(content + offset); switch (load->cmd) { case LC_SEGMENT: /* セグメント用コマンド */ { struct segment_command *segment; struct section *section; gint j; /* セグメント */ segment = (struct segment_command *)(content + offset); /* __TEXTセグメント以外は興味がない */ if (!g_str_equal(segment->segname, "__TEXT")) { /* セクション数だけ数えてスキップ */ section_index += section->nsects; break; } /* セクション */ section = (struct section *)(content + offset + sizeof(*segment)); for (j = 0; j < segment->nsects; j++, section++) { section_index++; /* __textセクションが何番目のセクションかを記録 */ if (g_str_equal(section->sectname, "__text")) text_section_index = section_index; } break; } case LC_SYMTAB: /* シンボルテーブル用コマンド */ { struct symtab_command *table; struct nlist *symbol; gchar *string_table; gint j; /* シンボルテーブルコマンド */ table = (struct symtab_command *)(content + offset); /* シンボルリスト */ symbol = (struct nlist *)(content + table->symoff); /* シンボル名が入っている文字列テーブル */ string_table = content + table->stroff; for (j = 0; j < table->nsyms; j++, symbol++) { gboolean defined_in_section = FALSE; /* シンボルがセクションで定義されているか */ if ((symbol->n_type & N_TYPE) == N_SECT) defined_in_section = TRUE; /* シンボルが__textセクションで定義されていて */ if (defined_in_section && symbol->n_sect == text_section_index && /* 公開されている */ symbol->n_type & N_EXT) { gchar *name; int32_t string_offset; string_offset = symbol->n_un.n_strx; /* 文字列テーブルからシンボル名を取得 */ name = string_table + string_offset; /* シンボル名の先頭に「_」がついているので2文字目以降を表示 */ g_print("found: %s\n", name + 1); } } break; } default: break; } /* 次のコマンドに進む */ offset += load->cmdsize; }
mach-o/loader.hとmach-o/nist.hを使って、BFDライブラリに依存せずに、Mach-Oから公開されている関数名を抜き出す方法を紹介しました。
関数名が取得できたら、GModuleで関数本体を取得することができます。GModuleに渡す関数名の先頭には「_」をつける必要はありません。
いずれ、PEから公開されている関数名を抜き出す方法も紹介するかもしれません。
先日、書きやすさとデバッグのしやすさを重視したC言語用テスティグフレームであるCutter 1.0.7がリリースされました。
Cutterでは、定義したテスト関数をフレームワークに登録する必要はありません。Cutterを用いたテストでは、共有ライブラリとしてテストを作成し、cutterコマンドでその共有ライブラリを読み込んで定義されているテスト関数を検出し実行します。
1.0.6までのCutterは、共有ライブラリから定義されているテスト関数を抽出するためにBFDライブラリを用いていました。しかし、共有ライブラリではなく静的ライブラリとしてBFDライブラリが提供されているプラットフォームがわりとあり、導入の障壁となる場合がありました。そこで、Cutter 1.0.7ではBFDライブラリに依存せず、共有ライブラリから定義されているテスト関数を抽出する機能を実装しました。
Cutter 1.0.7はELF/PE/Mach-Oに対応しているため、Linux, *BSD, Solaris, Cygwin, Mac OS Xなどの環境でもBFDライブラリなしで動作するようになりました。
ELFのフォーマットを解説しているページや、readelfなどのELF関連ツールを紹介しているページはあるのですが、ELFからシンボル名を抜き出すプログラムを紹介しているページがなかったので、Cutterで行っている、ELFから公開されている関数名を抜き出す方法を紹介します。
簡略化のためファイルの内容をすべてメモリに読み込んでから処理します。コツコツ資源を利用したい場合は少しづつ読み込みながら処理することになります。
ファイルの内容を読み込むにはGLibのg_file_get_contents()が便利です。
gchar *content;
gsize length;
g_file_get_contents(filename, &content, &length, NULL);
これで、contentの中にファイルの内容が格納されました。これを使って公開されている関数名を抜き出します。
ELFのフォーマットに関する情報はelf.hで定義されています。ELF をパースするときはelf.hを使うと便利です。ここでも、elf.hを使います。
#include <elf.h>
まず、ファイルがELFかどうかを判断します。
ELFは最初にヘッダが入っていて、それを見ることでELFかどうかを判断することができます。ここでは、64bit環境用のELFだけを対象とします。32bit環境用のELFを対象とする場合はコード中の「64」という箇所を「32」に変更します。どちらにも対応する場合はCutterのソースを参考にしてください。
Elf64_Ehdr *header = NULL; header = (Elf64_Ehdr *)content; if (memcmp(header->e_ident, ELFMAG, SELFMAG) == 0) { /* ELFファイル */ }
「MAG」は「マジック」の略だと思います。
ELFであることが確認できたら、共有ライブラリかどうかを確認します。
if (header->e_type == ET_DYN) { /* 共有ライブラリ */ }
.dynsymセクションには動的に解決されるシンボルが入っています。.dynstrセクションにはそれらのシンボルの名前が入っています。これらを見ることで共有ライブラリの中にあるシンボル名の一覧を取得することができます。
.textには関数の本体などが入っています。.dynsymにあるシンボルが.textセクションに関連していると、共有ライブラリ内で定義されているシンボルだということがわかります。
.dynsym/.dynstr/.textのセクションヘッダを探し出すコードは以下のようになります。
Elf64_Shdr *dynstr = NULL; Elf64_Shdr *dynsym = NULL; uint16_t text_section_header_index = 0; gsize section_offset; uint16_t section_header_size; uint16_t i, n_headers; Elf64_Shdr *section_name_header; gsize section_name_header_offset; const gchar *section_names; /* ファイルの先頭からセクションの先頭までのバイト数 */ section_offset = header->e_shoff; /* 1つのセクションヘッダのバイト数 */ section_header_size = header->e_shentsize; /* セクション数 */ n_headers = header->e_shnum; /* ファイルの先頭からセクション名があるヘッダの先頭までのバイト数 */ section_name_header_offset = header->e_shoff + (header->e_shstrndx * header->e_shentsize); /* セクション名があるヘッダ */ section_name_header = (Elf64_Shdr *)(content + section_name_header_offset); /* セクション名が格納されている位置の先頭 */ section_names = content + section_name_header->sh_offset; for (i = 0; i < n_headers; i++) { Elf64_Shdr *section_header = NULL; gsize offset; const gchar *section_name; /* ファイルの先頭からセクションヘッダの先頭までのバイト数 */ offset = section_offset + (section_header_size * i); /* セクションヘッダ */ section_header = (Elf64_Shdr *)(content + offset); /* セクション名 */ section_name = section_names + section_header->sh_name; if (g_str_equal(section_name, ".dynstr")) { /* .dynstrセクション */ dynstr = section_header; } else if (g_str_equal(section_name, ".dynsym")) { /* .dynsymセクション */ dynsym = section_header; } else if (g_str_equal(section_name, ".text")) { /* .textセクションが先頭から何番目のセクションか */ text_section_header_index = i; } }
.dynsym/.dynstr/.textのセクションヘッダが見つかったら、それらのセクションにアクセスして、共有ライブラリ内に定義されているシンボル一覧を取得できます。
公開されているシンボルが関数かどうかを判断する条件は、シンボルが.textセクションに関連付けられているかどうかです。
guint i, n_entries; gsize symbol_section_offset; gsize symbol_entry_size; gsize name_section_offset; /* ファイルの先頭からシンボルが定義されているセクションまでのバイト数 */ symbol_section_offset = dynsym->sh_offset; /* シンボル定義領域のバイト数 */ symbol_entry_size = dynsym->sh_entsize; /* ファイルの先頭からシンボル名が定義されているセクションまでのバイト数 */ name_section_offset = dynstr->sh_offset; /* シンボル定義領域の数 */ if (symbol_entry_size > 0) n_entries = dynsym->sh_size / symbol_entry_size; else n_entries = 0; for (i = 0; i < n_entries; i++) { Elf64_Sym *symbol; uint64_t name_index; unsigned char info; uint16_t section_header_index; gsize offset; /* ファイルの先頭からシンボル定義領域までのバイト数 */ offset = symbol_section_offset + (i * symbol_entry_size); /* シンボル定義 */ symbol = (Elf64_Sym *)(content + offset); /* シンボル名は何番目に定義されているか */ name_index = symbol->st_name; /* シンボルの情報 */ info = symbol->st_info; /* シンボルに関連するセクションは何番目のセクションか */ section_header_index = symbol->st_shndx; /* シンボルは関数に関連付けられている */ if ((info & STT_FUNC) && /* シンボルは公開されている */ (ELF64_ST_BIND(info) & STB_GLOBAL) && /* シンボルは.textセクションに関連付けられている */ (section_header_index == text_section_header_index)) { const gchar *name; /* シンボル名 */ name = content + name_section_offset + name_index; g_print("found: %s\n", name); } }
elf.hを使って、BFDライブラリに依存せずに、ELFから公開されている関数名を抜き出す方法を紹介しました。ELFを読み書きするlibelfというライブラリもあるのですが、ELFから情報を取得するだけなら、elf.hで十分でしょう。
関数名が取得できたら、GModuleで関数本体を取得することができます。GLibは便利ですね。
いずれ、PEまたはMach-Oから公開されている関数名を抜き出す方法も紹介するかもしれません。
プログラムを書いていると問題に遭遇します。問題に遭遇したときはエラーメッセージが問題解決の重要な情報になります。しかし、エラーメッセージがあるだけでは問題解決にはつながりません。問題解決に役立つエラーメッセージとそうでもないエラーメッセージがあります。
ここでは、Rubyでの例をまじえながら問題解決に有用なエラーメッセージを紹介します。ライブラリなど多くの人が使うようなプログラムを作成する場合は参考になるかもしれません。
問題に遭遇してから問題を解決するまでには以下の順で作業をする必要があります。
役立つエラーメッセージがあると「1. 問題の把握」、「2. 問題の原因の調査」、「3. 原因の解決方法の検討」がはかどります。
エラーが発生すれば問題が起こっている事実は把握できます。次にすることは、どのような問題が起こっているかを調査することです。
String#gsubにはいくつかの使い方がありますが、その1つは以下のように正規表現と文字列を引数にする使い方です。
>> "abcde".gsub(/c/, "C") => "abCde"
もちろん、違うオブジェクトを渡すとエラーが発生します。
>> "abcde".gsub([:first], [:second]) TypeError: can't convert Array into String from (irb):2:in `gsub' from (irb):2
配列を文字列に変換できなかったといっています。しかし、ここでは引数に配列を2つ指定しています。このエラーメッセージでは「配列を文字列に変換できなかった」ことはわかりますが、「どの配列を文字列に変換できなかった」かはわかりません。
正規表現のリテラルでも、正規表現の構文が間違っている場合はエラーが発生します。
>> Regexp.new("(") RegexpError: premature end of regular expression: /(/ from (irb):3:in `initialize' from (irb):3:in `new' from (irb):3
この場合は「正規表現に問題がある」というだけではなく、「どの正規表現に問題がある」かも示しています。
このように、問題を起こしたオブジェクトの情報も示すことで「問題を把握」しやすくなります。エラーメッセージには、問題を起こしたオブジェクトの情報も含めるようにしましょう。
問題が把握できたら、どうしてその問題が発生したのか、原因を調べます。多くの場合、エラーメッセージに問題の原因は書かれています。しかし、そうではない場合もあります。できるだけ、エラーメッセージには問題の原因も含めるようにしましょう。
Time.iso8601はISO 8601で定められた文字列のフォーマットをパースし、Timeオブジェクトにします。
>> require 'time' => true >> Time.iso8601("2009-04-10T12:02:54+09:00") => Fri Apr 10 03:02:54 UTC 2009
不正なフォーマットの場合はエラーが発生します。
>> Time.iso8601("2009-04-10I12:02:54+09:00") ArgumentError: invalid date: "2009-04-10I12:02:54+09:00" from /usr/lib/ruby/1.8/time.rb:376:in `iso8601' from (irb):6
この例では真ん中あたりの「T」が「I」になっているためフォーマットに適合していません。
もし、「『I』という不正な文字があります」というようなメッセージが入っていると、問題の原因を簡単に把握できるようになります。
エラーメッセージには大雑把な原因だけではなく、できるだけ詳しく原因を書くようにしましょう。
問題の原因がわかったら、その問題を解決する方法を検討します。期待している値がわかると、解決する方法を検討しやすくなります。
String#deleteは1つ以上の引数をとります。1つも引数を与えない場合はエラーが発生します。
>> "abcde".delete("a") => "bcde" >> "abcde".delete ArgumentError: wrong number of arguments from (irb):2:in `delete' from (irb):2
エラーメッセージを見ると「引数の数が違う」ということがわかります。これで「問題の原因」を把握することができます。
しかし、「問題の原因」はわかってもどうすればその問題を解決できるかはわかりません。引数の数を変えればよいということはわかりますが、いくつにすればよいかがわからないのです。
期待している値を示すと、問題を解決しやすくなります。
>> "abcde".gsub ArgumentError: wrong number of arguments (0 for 2) from (irb):3:in `gsub' from (irb):3
このエラーメッセージからはString#gsubが2つの引数を期待していることがわかるので、解決案として「引数を2つ渡す」というアイディアが浮かびます。次にすることは「引数に何を2つ渡すか」を考えることです。
エラーメッセージに「期待していること」を含めると、解決案が浮かびやすくなります。できるだけ、期待していることも含めるようにしましょう。
Rubyを例にして問題解決に役立つエラーメッセージについて紹介しました。
問題解決に役立つエラーメッセージの特長は、テストの実行結果にもあてはまります。クリアコードが開発に関わっているテスティングフレームワークではテストの実行結果にこだわっています。
あなたが使っているテスティングフレームワークは問題解決に役立つような情報を提供していますか?
先日、C言語用単体テストフレームワークであるCutterの新バージョン1.0.6がリリースされました。(アナウンス)
NEWSにも書いてあるように、今回のリリースでも多くの新機能がありますが、ここではその中でも特におすすめの構造体定義なしで複雑なテストデータを使えるAPIを紹介します。
この新機能によりもっと簡単にデータ駆動テストが書けるようになります。実際、milter managerのテストが簡単に書けるようになりました。
データ駆動テストとは同じテストケースに対して複数のテストパターンを適用するテスト手法です。これにより多くのテストパターンを簡単に書くことができるという利点があります。
例えば、入力された文字列の小文字をすべて大文字に変換するto_upper()関数のテストをするとします。テストデータは以下の3つを考えます*1。
これはこのように書けます。
cut_assert_equal_string("HELLO", to_upper("hello")); cut_assert_equal_string("HELLO", to_upper("HelLo")); cut_assert_equal_string("HELLO", to_upper("HELLO"));
データ駆動テストでは、テストケースはこのようになります。
cut_assert_equal_string(expected, to_upper(input));
このうち、expectedとinputを外部から与えることになります。Cutterではこのように書きます。
void data_to_upper (void) { #define ADD_DATUM(label, expected, input) \ gcut_add_datum(label, \ "expected", G_TYPE_STRING, expected, \ "input", G_TYPE_STRING, input, \ NULL) ADD_DATUM("all lower", "HELLO", "hello"); ADD_DATUM("mixed", "HELLO", "HelLo"); ADD_DATUM("all upper", "HELLO", "HELLO"); #undef ADD_DATUM } void test_to_upper (gconstpointer data) { const gchar *expected, *input; expected = gcut_data_get_string(data, "expected"); input = gcut_data_get_string(data, "input"); cut_assert_equal_string(expected, to_upper(input)); }
QtのQtTestLibではこのようになります。 Chapter 2: Data Driven Testingより:
void TestQString::toUpper_data(QtTestTable &t) { t.defineElement("QString", "string"); t.defineElement("QString", "result"); *t.newData("all lower") << "hello" << "HELLO"; *t.newData("mixed") << "Hello" << "HELLO"; *t.newData("all upper") << "HELLO" << "HELLO"; } void TestQString::toUpper() { FETCH(QString, string); FETCH(QString, result); COMPARE(string.toUpper(), result); }
ほとんど同じくらいの手間で書けているのではないでしょうか。
テストプログラムは重複があってもいいからわかりやすい方がよい、とよく言われます。しかし、そのために、重複がたくさんある中から、興味がある重複していない部分が見つけづらくなるのは問題かもしれません。データ駆動テストでは、どのようにテストしたいのかがわかりやすいテストケースになる傾向がある気がしています。
多くのテスティングフレームワークはデータ駆動テストをサポートしています。PerlのTest::Base、RubyのRSpec*2、C++のQtのQtTestLibやGoogle Testでもサポートされています。他にもまだまだたくさんあります。
テストデータの入力方法も様々で、テストプログラム中でテストデータを生成するものから、データベースからテストデータを取り出すもの、CSV、Excelなどから取り出すものもあります。
動的に複雑なことができるスクリプト言語、アノテーションなどでメタデータを指定できる最近の言語、トリッキーなC++などでは便利にデータ駆動テストを実行できるテスティングフレームワークは多くあります。しかし、C言語用のフレームワークではそんなになかったのではないかと思います。*3
Cutterに興味がある方は使ってみたり、メーリングリストなどで提案、質問などしてみてください。
ここには書いていませんでしたが、Cutter 1.0.3のリリース のリリースの約1ヶ月後に1.0.4がリリースされました。 さらにその約2ヶ月後の昨日、1.0.5がリリースされました。 CutterとはC言語用の単体テス トフレームワークです。
詳細は NEWSに 書いてありますが、1.0.5での目玉新機能は以下の3点です。
gdk-pixbufをサポートすることにより、画像が等しいかどうかを検 証できるようになりました。また、もし、画像が異なる場合は画像 間の差分を示し、画像のどこが異なるのかをわかりやすくしていま す。
例えば、このような画像を比較したとします。
これらの画像では赤い丸の部分が異なっています。
これらの画像の差分画像は以下のようになります。
左上に期待画像、右上に実際の画像、左下に異なるピクセルを示し た画像、右下に異なるピクセルを強調表示、同じピクセルを弱めに 表示した画像を配置しています。左下の画像を見ることでどの部分 が異なるのかが具体的にわかり、右下の画像を見ることで比較画像 はどのあたりが異なるのかを相対的に確認する事ができます。
もっとよい表現方法があるかもしれませんが、しばらくはこの方法 を採用しする予定です。もしかすると、今後、よりよい表現方法に 変更されるかもしれません。
ちなみに、この機能はPDF操作・レンダリングライブラリである Popplerのテスト で利用されていま す。
Cutterを使用していて、こんな検証・便利関数があったら便利だ、 と感じたものは積極的にCutter本体に取り込んでいます。
1.0.5では12個の検証、9個の便利関数が追加されました。追加され た検証・便利関数のリストは NEWSに 書かれています。
今までもユーザが独自で検証を定義することはできたのですが、バッ クトレースを取得するためにマクロとして定義する必要がありまし た。
1.0.5では cut_trace() というマクロが追加され、検証を関数として定義してもバックトレー スを取得することができるようになりました。マクロは可変長引数 が簡単に書けるなど便利な事も多いのですが、構文エラーが見つけ にくいなどという問題もあります。1.0.5からは関数とマクロを使い 分けられるようになり、より便利にデバッグのしやすいテストが書 けるようになりました。
Cutter 1.0.5ではテスト作成・デバッグ支援の機能が強化され、よ り便利なテスト環境を提供するようになりました。今までよりもC 言語でのテスト作成が楽しくなるかもしれません。
[チュートリアル ] [リファレンスマニュアル ] [ダウンロード ]
ちょうど1ヶ月前の話の続きです。
前回でCutterでテストを作成するための環境ができたので、実際にテストを作成していきます。と、思ったのですが、もう一点やらなければいけないことが残っていました。テスト対象のライブラリの初期化についてです。
今回はテスト対象ライブラリの初期化について説明してからテスト作成に入ります。
前回同様、コードの断片がでてきます。完全なものはSennaのリポジトリを見てください。
Sennaのようにライブラリ初期化・終了関数 (sen_init()/sen_fin())を用意している場合は、テストの作成に 入る前に、もう一つ用意しておかなければいけない仕組みがありま す。このような関数を持っているライブラリをテストする場合は、 テスト全体を実行する前に初期化関数を、実行した後に終了関数を 呼び出す必要があります。これを行う仕組みを用意する必要があり ます。
cutterコマンドは指定されたディレクトリ以下の共有ライブラリを かき集めて、その中からテストを検出して実行します。その時に以 下の条件にあう共有ライブラリを見つけると、テスト全体を実行す る前後に特定の関数を実行することができます。これは、今まさに 必要としている機能です。
この共有ライブラリが以下の名前のシンボルを公開している場合は、 その関数をテスト全体を実行する前後に実行します。ここでは、共 有ライブラリのファイル名はsuite_senna_test.soとします。
「_warmup」と「_cooldown」の前の「senna_test」の部分は共有ラ イブラリのファイル名から先頭の「suite_」と拡張子を除いた部分 です。
Sennaの場合は以下のようなsuite-senna-test.cを作成します。
test/unit/suite-senna-test.c:
#include <senna.h> void senna_test_warmup(void); void senna_test_cooldown(void); void senna_test_warmup(void) { sen_init(); } void senna_test_cooldown(void) { sen_fin(); }
suite-senna-test.cをビルドするためにMakefile.amに以下を追加 します。
test/unit/Makefile.am:
if WITH_CUTTER ... noinst_LTLIBRARIES = \ suite_senna_test.la endif INCLUDES = \ -I$(srcdir) \ -I$(top_srcdir) \ -I$(top_srcdir)/lib \ $(SENNA_INCLUDEDIR) AM_CFLAGS = $(GCUTTER_CFLAGS) AM_LDFLAGS = -module -rpath $(libdir) -avoid-version LIBS = \ $(top_builddir)/lib/libsenna.la \ $(GCUTTER_LIBS) suite_senna_test_la_SOURCES = suite-senna-test.c
よくあるMakefile.amの書き方です。noinst_LTLIBRARIESをif WITH_CUTTER ... endifの中に入れているのは、Cutterがない環境で はビルド対象からはずし、ビルドエラーにならないようにするため です。
これで、test/unit/.libs/suite_senna_test.soがビルドされるよう になり、テスト全体を実行する前後にSennaの初期化・終了処理を行 うことができます。
テスト実行環境が整ったのでテストを作成します。ここでは検索キー ワードの周辺テキストを取得するsnippet APIのテスト を1つ作成します。
テストの流れは以下の通りです。
基本的なCutterのテスト作成方法についてはチュートリアル を参考にしてください。
まずは、sen_snip_open()でsen_snipオブジェクトを生成する部分 と、sen_snip_close()で生成したsen_snipオブジェクトを開放する 部分を作成します。
今回はGLibサポート付きでCutterを使用するgcutterパッケージを 使っているので、テストは以下のようにgcutter.hを利用します。
test/unit/test-snip.c:
#include <senna.h> #include <gcutter.h> void test_simple_exec(void); static const gchar default_open_tag[] = "[["; static const gchar default_close_tag[] = "]]"; void test_simple_exec(void) { sen_snip *snip; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); sen_snip_close(snip); }
sen_snip_open()は引数が多いですが、ここでは気にする必要はあ りません。sen_snip_open()によりsen_snip *が生成されることだけ知っ ていれば問題ありません。
cut_assert_not_null(snip) でsen_snip *が正常に生成されているかを確認します。これは、 sen_snip_open()は失敗時にはNULLを返すからです。
最後にsen_snip_close()で生成したsen_snip *を開放します。
次はsen_snip_add_cond()でキーワードを指定する処理を追加しま す。sen_snip_add_cond()の戻り値はsen_rcです。sen_rcはエラー 番号を示す数値でsen_success(0)以外はエラーになります。よって テストは以下のようになります。sen_snip_open()のときと同じく、 sen_snip_add_cond()の引数は気にしなくても構いません。
test/unit/test-snip.c:
... void test_simple_exec(void) { sen_snip *snip; const gchar keyword[] = "Senna"; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); cut_assert_equal_int(sen_success, sen_snip_add_cond(snip, keyword, strlen(keyword), NULL, 0, NULL, 0)); sen_snip_close(snip); }
sen_snip_add_cond()の結果は cut_assert_equal_int で検証しています。
ただし、ここで問題があります。cut_assert*()は検証が失敗する とその時点でテスト関数からreturnし、それ以降のコードは実行し ません。つまり、cut_assert_equal_int()が失敗した場合は、 sen_snip_open()で生成したsen_snip *が開放されないことになり ます。この問題を解決するためにsetup()/teardown()という仕組み があります。
setup()はテストが実行される前に必ず実行され、teardown()はテ ストが実行された後に成功・失敗に関わらず必ず実行されます。こ の仕組みを利用することで確実にメモリ開放処理を行うことができ ます。
test/unit/test-snip.c:
... static sen_snip *snip; ... void setup(void) { snip = NULL; } void teardown(void) { if (snip) { sen_snip_close(snip); } } void test_simple_exec(void) { const gchar keyword[] = "Senna"; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); cut_assert_equal_int(sen_success, sen_snip_add_cond(snip, keyword, strlen(keyword), NULL, 0, NULL, 0)); }
これでcut_assert_equal_int()が成功しても失敗してもsen_snip * は開放されます。Cutterではメモリ開放処理のためにstatic変数と setup()/teardown()を使うことが定石になっています。
次はsen_snip_add_cond()で設定したキーワード用のsnippetを生成 するsen_snip_exec()のテストです。sen_snip_exec()もsen_rcを返 すので、それを検証します。また、引数でsnippet数とsnippet文字 列のバイト数も受けとるのでそれも検証します。特に目立った部分 はありません。
test/unit/test-snip.c:
... static const gchar text[] = "Senna is an embeddable fulltext search engine, which you can use in\n" "conjunction with various scripting languages and databases. Senna is\n" "an inverted index based engine, & combines the best of n-gram\n" "indexing and word indexing to achieve fast, precise searches. While\n" "senna codebase is rather compact it is scalable enough to handle large\n" "amounts of data and queries."; ... void test_simple_exec(void) { ... unsigned int n_results; unsigned int max_tagged_len; ... cut_assert_equal_int(sen_success, sen_snip_exec(snip, text, strlen(text), &n_results, &max_tagged_len)); cut_assert_equal_uint(2, n_results); cut_assert_equal_uint(105, max_tagged_len); }
最後はsen_snip_exec()で生成したsnippetの内容が正しいかどうか のテストです。snippetはsen_snip_get_result()で取得できるので その結果を検証します。n_resultsが2なので2回 sen_snip_get_result()を呼び出す必要があります。
snippetを格納する場所のサイズは動的に決まります。そのため、 snippetを格納する領域を動的に確保する必要があります。 setup()/teardown()の仕組みを用いてメモリを開放するようにしま す。ここ以外は特に目立った部分はありません。
test/unit/test-snip.c:
... static gchar *result; void setup(void) { ... result = NULL; } void teardown(void) { ... if (result) { g_free(result); } } void test_simple_exec(void) { ... unsigned int result_len; ... result = g_new(gchar, max_tagged_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 0, result, &result_len)); cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, " "which you can use in\n" "conjunction with various scripti", result); cut_assert_equal_uint(104, result_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 1, result, &result_len)); cut_assert_equal_string("ng languages and databases. [[Senna]] is\n" "an inverted index based engine, & combines " "the best of n-gram\ni", result); cut_assert_equal_uint(104, result_len); }
これで単純にsnippet APIを使った場合のテストが1つできました。 同様に、異常な場合や違ったデータを用いた場合などのテストを作 成していきます。
せっかくなのでCutterのテスト結果の出力方法を紹介します。
Cutterは cut_assert_equal_string で文字列の比較が失敗したときには、どの部分が異なったかという 差分情報を表示します。
例えば、今回のテストの最後のcut_assert_equal_string()が失敗 した場合は以下のような差分情報が表示されます。
diff:
- ng languages and DBes. [[Senna]] is
? ^^
+ ng languages and databases. [[Senna]] is
? ^^^^^^^
- an Inverted Index Based Engine, & combines the best of n-gram
? ^ ^ ^ ^
+ an inverted index based engine, & combines the best of n-gram
? ^ ^ ^ ^
i
このときの期待した結果は以下の通りです。
ng languages and DBes. [[Senna]] is an Inverted Index Based Engine, & combines the best of n-gram i
実際の結果は以下の通りです。
ng languages and databases. [[Senna]] is an inverted index based engine, & combines the best of n-gram i
差分を見てもらうと分かる通り、異なっている行を示すだけではな くて、行内で異なっている文字まで示しています。(例えば、DBの 下に^^が付いている。)
広く使われているunified diff形式では行内で異なる文字は表示し ません。テストでは1行のみの比較を行うことも多く、行単位だけ の差分よりも文字単位での差分表示も行った方がデバッグが行いや すいという判断からこのような形式になっています。
この形式はPythonのdifflibにあるndiffの形式と同じものです。
今回作成したテストは以下の通りです。
test/unit/test-snip.c:
#include <senna.h> #include <gcutter.h> void test_simple_exec(void); static sen_snip *snip; static const gchar default_open_tag[] = "[["; static const gchar default_close_tag[] = "]]"; static const gchar text[] = "Senna is an embeddable fulltext search engine, which you can use in\n" "conjunction with various scripting languages and databases. Senna is\n" "an inverted index based engine, & combines the best of n-gram\n" "indexing and word indexing to achieve fast, precise searches. While\n" "senna codebase is rather compact it is scalable enough to handle large\n" "amounts of data and queries."; static gchar *result; void setup(void) { snip = NULL; result = NULL; } void teardown(void) { if (snip) { sen_snip_close(snip); } if (result) { g_free(result); } } void test_simple_exec(void) { const gchar keyword[] = "Senna"; unsigned int n_results; unsigned int max_tagged_len; unsigned int result_len; snip = sen_snip_open(sen_enc_default, 0, 100, 10, default_open_tag, strlen(default_open_tag), default_close_tag, strlen(default_close_tag), NULL); cut_assert_not_null(snip); cut_assert_equal_int(sen_success, sen_snip_add_cond(snip, keyword, strlen(keyword), NULL, 0, NULL, 0)); cut_assert_equal_int(sen_success, sen_snip_exec(snip, text, strlen(text), &n_results, &max_tagged_len)); cut_assert_equal_uint(2, n_results); cut_assert_equal_uint(105, max_tagged_len); result = g_new(gchar, max_tagged_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 0, result, &result_len)); cut_assert_equal_string("[[Senna]] is an embeddable fulltext search engine, " "which you can use in\n" "conjunction with various scripti", result); cut_assert_equal_uint(104, result_len); cut_assert_equal_int(sen_success, sen_snip_get_result(snip, 1, result, &result_len)); cut_assert_equal_string("ng languages and databases. [[Senna]] is\n" "an inverted index based engine, & combines " "the best of n-gram\ni", result); cut_assert_equal_uint(104, result_len); }
問題発生時に有用なデバッグ情報を増やしたり、より読みやすいテ ストにするなど、いろいろ改良するべき点は残っていますが、今回 はこれで終了します。実際のコードはSennaのリポジトリを参照 してください。
2回に分けて以下のことについて説明しました。
Cで書かれたプロジェクトに単体テストフレームワークを導入する 場合はCutterも検討してみてはいかがでしょうか。
Sennaの単体テストフレームワー クとしてCutterを導入したときの手順です。自分のプロジェクトに Cutterを導入するときの参考になるかもしれません。全体として そこそこ長くなってしまったので、何回かに分割して紹介することに します。
内容はSennaのリポジトリ でやったことの一部です。リポジトリは公開されているので、試行錯誤の 後などをみたい場合はコミットを追いかけるとよいでしょう。また、ここで は断片としてしか出てこないコードについても、リポジトリの中には完全な 形で入っています。
もし、まだCutterについて知らない場合は、はじめにチュートリ アル を読んでください。
まず、Sennaについて簡単に説明します。
Sennaは組み込み型の全文検索エンジンで、その機能をライブラリ として提供します。SennaのAPIはbasic APIやadvanced APIなどい くつかのグループにわかれています。
今回はSennaの単体テストフレームワークとしてCutterを導入し、 utility APIのひとつ、snippet*1のテストを 作成するまでを示します。このためには以下の作業が必要になりま す。
作業に入る前にSennaのビルドシステムについて確認します。
SennaではGNU Automakeや GNU Libtoolな どGNUビルドシステムを利用したビルドシステムを採用しています。
CutterはGNUビルドシステムサポート用の機能をいくつか提供してい ます。そのため、GNUビルドシステムを用いているプロジェクトへ はCutterを容易に導入することができます。
もし、これからプロジェクトを始める場合でGNUビルドシステムを 採用する場合はCutterのチュートリアル が参考になるでしょう。
Sennaの単体テストフレームワークとしてCutterを採用するにあたっ て、以下のような条件を満たすこととします。
上記の中でのユーザと開発者の違いは、autogen.shを用いて自分で configureを作成するかどうかです。ユーザは開発者が作成した configureを利用するため、自分でconfigureを作成しません。一方、 開発者はSubversionリポジトリ内にはconfigureは入っていないの でautogen.shを使ってconfigure.acからconfigureを作成し、利用 します。つまり、違いは以下の通りになります。
それでは、まずは、開発者はすべてCutterをインストールしている ものとしてCutter対応のconfigureを生成できるようにします。
Cutterはconfigure.ac内で利用できるCutter検出用のM4マクロを cutter.m4として提供しています。このファイルは ${PREFIX}/share/aclocal/cutter.m4としてインストールされます。 ${PREFIX}/share/aclocal/以下に他の.m4ファイルがインストールされ ているような環境ではおそらくそのままで大丈夫ですが、そうでな い場合はautogen.shの中でaclocalを呼び出しているところを編集 して${PREFIX}/share/aclocal/以下を.m4ファイルの検索パスに加 える必要があります。
もし、Cutterのconfigureに--prefix=/tmp/localオプションをつけ てビルド・インストールした場合はautogen.shを以下のように変更 する必要があります。
Index: autogen.sh =================================================================== --- autogen.sh (リビジョン 820) +++ autogen.sh (作業コピー) @@ -105,7 +105,7 @@ echo "Running libtoolize ..." $LIBTOOLIZE --force --copy echo "Running aclocal ..." -$ACLOCAL ${ACLOCAL_ARGS} -I . +$ACLOCAL ${ACLOCAL_ARGS} -I . -I /tmp/local/share/aclocal echo "Running autoheader..." $AUTOHEADER echo "Running automake ..."
あるいはautogen.shを実行する時に環境変数ACLOCAL_ARGSを指定し ます。
% ACLOCAL_ARGS="-I /tmp/local/share/aclocal" ./autogen.sh
これでconfigure.ac内でCutterが提供する便利M4マクロを利用する 準備が整いました。
Cutterはパッケージを pkg-configのパッ ケージとしてインストールします。パッケージをpkg-configのパッ ケージとして作成しているのは、pkg-configが広く普及していて、 GNUビルドツールなどpkg-configに対応しているビルドシステムが 多いからです。
Cutterは、テスト作成用に以下の2つのパッケージを用意しています。
今回はGLibを利用してテストを作成するので、cutterパッケージで はなくgcutterパッケージを利用します。
Cutterはconfigure.acで簡単にcutter/gcutterパッケージの設定を 行えるように以下のM4マクロを提供しています。
cutterパッケージ検出マクロです。以下の変数をAC_SUBSTしま す。
また、cutterパッケージが利用不可能な場合は ac_cv_use_cutterが"no"になります。
今回はGLibサポートがついたgcutterパッケージを利用するので、 AC_CHECK_GCUTTERマクロを利用します。よってconfigure.acには以 下を追加することになります。
configure.ac:
AC_CHECK_GCUTTER AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"]) if test "$ac_cv_use_cutter" != "no"; then AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter]) fi
これで、Makefile.amではCutterが利用できるかどうかはif WITH_CUTTER ... endifで判断できます。Makefile.amではCutterが 利用できない場合はテストプログラムをビルドしないようにします。 こうすることにより、ユーザがCutterをインストールしていなくて も、Sennaをビルドできます。
cutter.m4がない場合は./autogen.shの実行が失敗します。つまり、 開発者がconfigureを正常に生成できなくなります。
残念ながら、Cutterはそれほど有名なフリーソフトウェアではない ため、開発者がCutterをインストールしていることはほとんどあり ません。そこで、開発者がCutterをインストールしていなくても configureを生成できるようにします。*2
cutter.m4がインストールされているかどうかはAC_CHECK_GCUTTER 関数が定義されているかどうかでわかります。そのため、以下のよ うに書くことにより、Cutterがインストールされてない環境でも configureを生成できます。もちろん、生成されたconfigureには Cutterの検出機能などはありません。
configure.ac:
m4_ifdef([AC_CHECK_GCUTTER], [ AC_CHECK_GCUTTER ], [ac_cv_use_cutter="no"]) AM_CONDITIONAL([WITH_CUTTER], [test "$ac_cv_use_cutter" != "no"]) if test "$ac_cv_use_cutter" != "no"; then AC_DEFINE(WITH_CUTTER, 1, [Define to 1 if you use Cutter]) fi
このようにAC_CHECK_GCUTTERの呼び出し部分をm4_ifdefの中に入れ るだけです。AC_CHECK_GCUTTERが定義されていない場合は ac_cv_use_cutterを"no"にしているのでWITH_CUTTERが真になるこ とはありません。
Cutterを用いたテストプログラムはtest/unit/以下に配置します。 このディレクトリは新規に作成するため、以下の作業が必要になり ます。
まずは、test/Makefile.amのSUBDIRSにunitを追加し、test/unit/ 以下もビルド対象とします。
test/Makefile.am:
SUBDIRS = unit
続いて、configure.acのAC_CONFIG_FILESにtest/unit/Makefileを 追加し、configureがtest/unit/Makefileを生成するようにします。
configure.ac:
AC_CONFIG_FILES([... test/unit/Makefile ...])
最後に、test/unit/Makefile.amを作成し、test/unit/以下のビル ド方法を設定します。とりあえず、今は空っぽでかまいません。
% touch test/unit/Makefile.am
これで、test/unit/以下をSennaのビルドシステムに加えることがで きました。再度./autogen.sh, ./configureを実行してからmakeす れば、test/unit/以下もビルド対象になっていることがわかります。
% ./autogen.sh % ./configure % make ... make[3]: ディレクトリ `.../test/unit' に入ります ...
test/unit/以下がビルド対象に加わったので、test/unit/以下に作 成するテストプログラムを起動するコマンドを作成します。このテ スト起動コマンドはmake checkから呼び出されることになります。
テスト起動コマンドは伝統的にrun-test.shというシェルスクリプ トになっています。このシェルスクリプトからcutterコマンドを呼 び出してテストを実行します。
cutterを実行するときはいくつかオプションを指定する必要があり ます。例えば、テストプログラムがあるディレクトリなどがそれで す。ここでrun-test.shを作成する理由は、cutterへ渡すオプション などを指定しなくてもよいようにするなど、より簡単にテストを実 行できるようにするためです。
テストが簡単に実行できるということはとても重要なことです。テ ストを実行することが面倒だと、だんだんテストを実行しなくなっ てしまうからです。テストが実行されないと、新しくテストを作成 することも面倒になってくるでしょう。これは悪い循環といえます。 これを防ぐためにも最初のうちから簡単にテストを実行できる仕組 みを用意しておくことが重要です。
また、引数なしでも動くrun-test.shを用意することにはもう一つ理 由があります。それは、GNU Automakeが提供するテスト起動の仕組 みであるmake checkからも利用できるようにすることです。make checkでは指定されたテスト起動スクリプトが引数なしでテストを実 行できる必要があります。*3
前置きが長くなりましたがテストをもっと簡単に走らせるためのス クリプト、run-test.shは以下のようになります。
test/unit/run-test.sh:
#!/bin/sh export BASE_DIR="`dirname $0`" if test x"$NO_MAKE" != x"yes"; then make -C $BASE_DIR/../../ > /dev/null || exit 1 fi if test -z "$CUTTER"; then CUTTER="`make -s -C $BASE_DIR echo-cutter`" fi if test x"$CUTTER_DEBUG" = x"yes"; then CUTTER="$BASE_DIR/../../libtool --mode=execute gdb --args $CUTTER" fi CUTTER_ARGS="-s $BASE_DIR" $CUTTER $CUTTER_ARGS "$@" $BASE_DIR
このスクリプトではmake check以外からも便利に利用できるように なっています。make check以外から起動された場合(つまり直接 test/unit/run-test.shを軌道した場合)は必要なビルドを行ってか らテストを起動します。つまり、run-test.shからテストを起動した 場合はビルド忘れがなくなります。
実は、上記のrun-test.shを直接起動できるようにするためには、 test/unit/Makefile.amにも一工夫する必要があります。それは、 configureで検出したcutterコマンドのパスをrun-test.shに伝える ためのターゲットを用意するということです。
test/unit/Makefile.am:
echo-cutter:
@echo $(CUTTER)
これで、run-test.shを直接起動しても、必要に応じてビルドした り、情報を集めたりしてテストを起動してくれます。
また、make checkではテスト結果とビルド結果が混ざりそこそこの 出力になりますが、run-test.sh経由でビルド・テストを行うと必 要最小限の出力になり、問題の発見が簡単になります。実際の開発 は以下のようなサイクルになります。
test/unit/run-test.shを実行
テスト失敗→(1)に戻る
手順が少ないため開発のリズムが崩れにくくなります。このサイク ルをより簡単に行うための方法もあるのですが、それはまた別の機 会にします。
run-test.shができたので、make checkでrun-test.shを起動するよ うにMakefile.amを変更します。
test/unit/Makefile.am:
if WITH_CUTTER TESTS = run-test.sh TESTS_ENVIRONMENT = NO_MAKE=yes ... endif
TESTS_ENVIRONMENTにNO_MAKE=yesを指定することにより、make check経由の場合はテスト実行前のmake実行を抑制します。
これでテストを実行するための環境は整いました。きりがよいので 今回はここまでにします。
ここまでで、以下のことについて説明しました。
続きではテストを作成します。
*1 検索キーワードの周辺テキストの こと。ここではそれを取得するSennaの機能のこと。
*2 本当は開発者には頻繁に テストを走らせて欲しいのでCutterを必須にしたいところです。
*3 テスト起動スクリプトにオプションを 指定する場合は環境変数を利用します。
昨日、C言語用の単体テストフレームワークである Cutterの1.0.3がリリースされま した。
実は、Cutter-1.0リリースから3回リリースしていま す。1.0.0以降はマイクロバージョンだけを上げていますが、新しく 追加された機能はマイクロとは思えません。例えば、Windows (MinGW)でのビルド に対応、GStreamer のサポートなどといった機能が含まれていました。過去のリリースに ついてはNEWS を見てください。
Cutterはテストの書きやすさ・テスト結果からのデバッグのしやす さを重視したC言語用の単体テストフレームワークです。今回のリリー スからCutterの機能を説明したページ を用意 しました。
同じテストを条件を変えて実行したい時があります。例えば、以下 のような場合です。
このような場合、必要な分だけテストコードをコピー&ペーストして テストを作成するよりも、以下のように書けるとテスト記述・管理 のコストを下げることができます。
このようなテストの方法をデータ駆動テストと呼びます。
データ駆動テストではデータの用意の仕方にはいくつかの方法があ り、それぞれ利点があります。
Cutterでは今回のリリースで、最後の「プログラム内で入力データ を生成」する方法をサポートしました。使い方は以下の通りです。
今までどおり、関数を定義するだけでよく、他のC言語用の単体テ ストフレームワークにあるような「登録処理」のようなことは必要 ありません。Cutterが自動で見つけてくれます。
コードにすると以下のようになります。
void data_XXX(void) { cut_add_data("データ1の名前", データ1, データ1をfreeする関数, "データ2の名前", データ2, データ2をfreeする関数, "データの例", strdup("test data"), free, ...) } void test_XXX(const void *data) { /* dataはdata_XXX()で登録した「データ1」か「データ2」 か「strdup("test data")」。test_XXX()はそれぞれに対 して1回ずつ、計3回呼ばれる。 */ cut_assert_equal_string("test data", data); }
具体例は cut_add_data() を見てください。
Cutter 1.0.3ではデータ駆動テストをサポートし、より簡単にテス トがかけるようになりました。
昨年の10月から開発しているCutterというC言語用の単体テストフレームワークのバージョン1.0が昨日リリースされました。
フリーソフトウェアの世界には簡単にテストを書けるC言語用の単体フレームワークがそれまではありませんでした。例えば、GLibに含まれているGTesterでは以下のようにmain関数を定義してその中で実行したいテストを一つづつ登録しなくてはいけません。
GLibのtests/testingbase64.cから抜粋:
static void test_base64_encode_decode (void) { ... } int main (int argc, char *argv[]) { g_test_init (&argc, &argv, NULL); g_test_add_func ("/misc/base64/decode", test_base64_decode); return g_test_run (); }
Cutterではこのような冗長な記述をさけるため以下のようなルールでテストが書かれていることを仮定しています。
このようなルールを定めることによって、main関数を書く必要がなく、テストを登録する関数も呼ぶ必要がないために、より楽にテストを書けるようになっています。
Cutterでは上記のテストは以下のように書くだけですみます。
void test_base64_encode_decode (void); /* prototype */ void test_base64_encode_decode (void) { ... }
テスト関数はダイナミックリンクライブラリとして読み込まれて実行される必要があるのでstaticな関数ではなくなってプロトタイプ宣言を書く必要がありますが、それでもGTesterに比べて簡潔に書けるようになっています。
Cutterではその他にもアサート文が充実してることなど他の単体テストフレームワークよりもかなり使いやすくなっています。Cでテストを書かなくてはいけなくなった時にはぜひ使ってみてください。