いまからでも間に合う開発者テスト
はじめまして。開発部じゃない加藤和良です。
最近、mixi では Buildbot をつかった継続的インテグレーションをはじめています。安定版の mixi のソースコードにコミットすると Buildbot がそれを検知し、自動的にテストが走るようになりました。
ここでの「テスト」は Test::Simple や prove(1) をつかった、Perl でかかれた開発者テストを指しています。mixi の開発者テストをとりまく環境は、ここ数年でかなり改善されました。今回はその歩みをふりかえりながら、テストの無いコードベースをどこからどうやって変えていったかという話をしたいと思います。
開発環境
はじめに、前提となる mixi の開発環境について説明します。mixi では複数人の開発者がひとつのマシンで作業を行います。それぞれの開発者は、あらかじめ割り当てられたポートで Apache を起動し、自分の好きなエディタでソースコードを編集しながら生活しています。
memcached は各マシンに一台。そして、沢山の MySQL 群や、数台の Tokyo Tyrant, Shindig などは社内のほぼ全員が同じものを共有しています。
当然のことながら、これらのサーバーは皆さんがお使いの 本番の mixi で使っているものとは、別のものです。以下で「日記」とか「ボイス」とか書いてあるのは、開発者が開発環境でつくっているデータをさすことに、あらかじめ注意してください。
2007年4月 - t/ の追加と当時の問題
mixi のソースコードは以下のように配置されています。
lib/, t/ があるのは CPAN モジュールでもありがちですが、t/ の下に lib/ と同じような階層構造があるのは、ちょっと珍しいかもしれません。
- /
- lib/
- Mixi/Application/Object.pm
- t/lib/
- Mixi/Application/Object/leave.t
- ...
- lib/
この t/ フォルダは、2007年4月ごろにレポジトリに追加されました。ここから mixi の開発者テストの歴史がはじまるわけですが、当時のテストまわりにはいくつかの問題がありました。
一般に、テストを書きやすいコードは、以下のような性質を持っていると思います。
- 結果を左右する入力をどこから与えるべきかわかりやすい
- 結果がどこに出力されているかわかりやすい
- それ以外の副作用が無い
たとえば、文字列のうち HTML 上で特殊な意味をもつものをエスケープする関数や
is(escape('foo & bar'), 'foo & bar');
スタックを表現するクラスなどは「テストを書きやすい」部類のコードと呼べます。
my $stack = Stack->new; ok(! $stack->pop); $stack->push(123); is($stack->pop, 123);
しかし、当時の mixi のコードベースには、前述の性質をそなえていないものが多くありました。
- 入力として、どこかの環境変数を読んでいたり、どこかのデータベースの情報を読んでいたり、new しづらいクラスを使っている
- 出力が標準出力への print だったり、データベースのどこかへの更新や削除だったり、例外処理で exit してしまったりしている
- 副作用としてどこかのデータベースや memcached に更新や削除を行ってしまう
結果、テストを書くのがむずかしかったり、せっかく書いたテストが、誰かが日記を消したり、マイミク関係を解消したのをきっかけに突然失敗するようになったり、テストを実行したらどこかのボイスの投稿が消えるかも、みたいな不安で他人が書いたテストを動かしづらかったり、といったことが起こりがちでした。
2008年12月 - Mixi::Test::Fixtures
これらの問題を解決するべく、2008年12月ごろ Mixi::Test::Fixtures という新しいライブラリが追加されました。
Mixi::Test::Fixtures は、Rails や Django にある "fixture" の仕組みを、mixi のコードベースに導入するためのライブラリです。具体的には、以下のような動作を行います。
- テストの実行直前に、一時的なデータベースを作成
- 指定された YAML から、データベースに値を挿入
- テストの実行
- テストの実行直後に、いろいろ変更のあったデータベースを削除
mixi は OR マッパを使っていない部分もいろいろあり、手書きの SQL が MySQL を期待しているところが多々あります。そのため、一時的なデータベースにも SQLite のようなインプロセスのデータベースは使わず、MySQL をそのまま使っています。テストの実行前後に CREATE DATABASE / DROP DATABASE が走るのはなかなか富豪的です。
Mixi::Test::Fixtures の導入によって
- テストが、いつ変わるかわからないデータベース上の情報に依存している
- テスト実行後にデータベース上に様々なデータが作成・削除され、2回目以降の実行では前提となる条件が変わってしまう
といった問題は回避できるようになりました。
その後、何度かのバージョンアップがあり、現在ではデータベース以外に
- memcached
- メールの送信
- 外部への HTTP アクセス
も Mixi::Test::Fixtures で差し替えが可能です。これらの動作を模倣するのはデータベースに比べるとずっと簡単なので、インプロセスで代替のコードが動くようになっています。
2009年4月 - ブラックリストの導入
Mixi::Test::Fixtures 導入以後、新しいテストについては、だれでもいつでも安心して実行できるようになりました。ただ、古いテストのなかには、まだ fixture への移行がすんでいないものもあり、気軽に実行するのは不安がありました。
一方で、すべてのテストを実行したいという要求もありました。コードの変更が予想外の部分をこわしていないかを確かめるためには、なるたけ多くのテストを実行したほうが安心です。
この中途半端な状態をなんとかするために2009年4月ごろ導入されたのが、ブラックリストと、それを使って「ホワイトな」テストを列挙する仕組みです。
まず t/blacklist.yaml に
patterns: - "^t/lib/Mixi/Official/Member/.*$" files: - t/lb/Mixi/Application/DB/Ad.t ...
と、だれもが気軽に実行するのがまだ難しいテストを列挙していきます。これを元に list-tests というスクリプトが
% ./script/devel/list-tests t/lib/Mixi/Application/Object/leave.t ... %
t/ 以下をなめて blacklist.yaml にマッチしないテストの一覧を出力します。これを
% prove -lv `./script/devel/list-tests` ....
といった感じで prove(1) にわたすことで、誰もが何時でも実行できるテストだけを、簡単に実行することができます。列挙までして prove(1) の実行は行わないのは
% prove -lv `./script/devel/list-tests | grep Application` ....
のように他のソフトウェアとの連携を考えてのものです。
ブラックリストの導入に関しては
- 実行に注意が必要なテストはだめなので、修正するか消す
- t/, xt/ などテストをいれるフォルダを二つに分ける
- 個々のテストに plan skip_all => '...'; みたいなものを記述する
というやりかたもあったと思います。
ただ、1 は修正が完了するまでテストの全実行をあきらめなくてはいけないこと、かといって締切を設定し、それまでに修正されないテストは消す、となるとテストが消えすぎること、そもそも「実行に注意が必要なテストはだめ」というのが微妙で、書いた人しか実行できないテストでもないよりはマシなことを考えて、無しになりました。
2 は、ファイルの移動に Subversion が弱いことと、2つの分割ですまなくなったときにどうしよう、というのが不安なところでした。
3 については、柔軟で良さげなので、将来的にはここに落ち着くんじゃないかと思っています。いまになって思えば、最初からこれでも良かったかもしれないですね。
2009年5月 - Test::Apache2
Mixi::Test::Fixtures とブラックリストは mixi 全体にかかわる部分でした。ここからは、各々のコードに関わる細かな部分に目をむけてみようと思います。
Test::Apache2 は mod_perl ハンドラのテストを書くためのライブラリです。目的だけみれば Apache::Test によく似ていますが、本物の Apache を立ち上げないでインプロセスで動くところが大きなちがいです。mod_perl 特有の細かな状況 (具体的にはあんまり把握していません) についてテストを書くのには向きませんが、反面、実行速度が速く準備も簡単です。
Test::Apache2 は2009年の5月ごろに mixi の安定版で使えるようになりました。現在では mixi から切り出して CPAN 上で配布しているので、mixi の外でも使えます。
既存の mod_perl ハンドラの挙動をとりあえずテストで囲いたい、みたいなときに、ちまちま Test::MockObject で Apache2::RequestRec を模倣するよりは便利だと思うので、ぜひ試してみてください。
2009年12月 - Test::Exit
Test::Exit は exit の呼び出しをテストするためのライブラリで、Andrew Rodland さんが開発されています。
... exits_nonzero { invalid_op(); # なかで exit(1) する }; done_testing;
こんなふうにつかえます。mixi では例外処理のなかで exit してしまう CGI ノリのコードがごくまれにあり、そういうものに対処するために2009年12月ごろにつかいはじめました。
Test::Exit には当時 exit($status); のみ対応していて exit; のような引数なしの呼び出しをうまく扱えないバグがあったのですが、これはパッチを送って 0.03 で直してもらいました。
こういうごくニッチなところにも先人がいるのは、CPAN というか Perl のすごいところだなあと思います。
まとめ
というわけで、mixi の開発者テストのいままでの歩みをふりかえってみました。今年に入ってからは Buildbot の設定をいろいろ詰めているのですが、これはまだ固まりきっていないので、紹介はまた次の機会に。
mixi の開発者テストまわりの仕組みは、基本的に「テストのことを考えずにだらだらコードを書いても、後から挽回できるように」できています。できているというか、そうせざるをえなかった、というのが実際のところです。
そのため、ださいところや、無理矢理なところも多々ありますし、実行速度も結構遅かったりします。ただ「テスト書きたいのは山々なんだけど、既存コードに手をいれるのはきつくて、次の機会をうかがっている」というありがちな状況からは一歩抜け出せました。「おれ、新しいフレームワーク (or ライブラリ or 言語) がきたら、次はテスト書くんだ」なんてのは、良くない類のフラグを立てているだけであると、個人的には思います。