前回に引き続き、クリアコードインターン記事の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 を使うと、以下のように簡単にコマンドを実行することができます。
1 2 3 |
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 で準備しておくとよいでしょう。
1 2 |
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 時に自動でオブジェクトを解放してくれるという便利機能があります。どうやるかというと、下記のようにするだけです。
1 2 |
cut_take(client, g_object_unref); cut_take(egg, g_object_unref); |
またこれらは、GLibをサポートしたGCutterの関数を使うと、
1 2 |
gcut_take_object(G_OBJECT(client)); gcut_take_object(G_OBJECT(egg)); |
と書くこともできます。 これで client と egg は自動的に tear down 時に解放されるようになります。Cutterでは適切な下準備をしておくと、tear down 用の関数でわざわざ後片付けをしなくてもOKです。便利ですね。
ここまでをまとめると、テストのセットアップは次のように書くことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 を返してくるのでこれをテストしてみます。
1 2 3 4 5 6 7 8 9 |
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 という関数を提供しています。
1 2 3 4 5 6 7 8 9 10 |
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 では次のように簡潔に書くことができます。
1 2 3 4 5 6 7 8 |
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 関数名がやや長いのは御愛嬌ということで ;-)