はじめに
はじめまして。(株)ミクシィの加藤和良です。2008年度に入社し、2011年1月からはシステム本部 技術部に所属しています。技術部は、日記やコミュニティといった特定のサービスに紐づかない、mixi全体を裏から支える部署です。「支える」ための方法は、実際のサービスの一部として動作する共通基盤から、開発効率を上げるために社内で動作しているものまで、多岐にわたります。
mixiでは、ここ数年で自動テストの導入が急速に進みました。図1は、mixiのソースツリーにおけるコードと、そのテストコードの毎月1日のバイト数をグラフにしたものです。2008年の頭には少なかったテストが急速に増え、今年の5月にはコード量をも追い越しているのがわかります。
携帯電話向けmixiである「mixiモバイル」の開始が2004年、mixiニュースが2006年ですから、2008年当時のmixiも、それなりに大きなアプリケーションであったと言えます。今回は、そのような比較的大きく、歴史あるコードの中に、どうやって自動テストを浸透させていったのか、ある一例をお話していきます。
データベースとの依存を切り離す
mixiでは、ほぼすべての開発者がSSH経由でデータセンターのLinuxマシン上に接続しています。マシンは複数のVMに切り分けられ、それぞれ別の開発者が使用します。個々の開発者は、mixiのフロントエンドにあたる部分だけをそのVM上で起動し、MySQLのようなデータベース、memcached、画像配信系をはじめとするサブシステムについては、ほぼすべての開発者が同じものを共有しています。
mixi全体に自動テストを導入する際に、最初に障壁となったのがデータベースでした。一般に、テストを書きやすいコードには次のような性質があります。
- 結果を左右する入力をどこから与えるべきかが明確である
- 結果がどこに出力されるかが明確である
- それ以外の副作用を持たない
しかし、2008年ごろのmixiのコードには、これらを備えていないものが多くあり、とりわけデータベースは、テストに対する暗黙の入力であり、出力先になってしまうことがしばしばありました。誰かが友人関係を解消したり、日記を削除したり、といった行動によってテストが失敗してしまうようでは、テストが失敗することは日常茶飯事になり、テストを書く利点が減ってしまいます。また、テスト自体はうまく動作していても、副作用として、開発環境上の誰かの日記が増えたり、どこかのコミュニティのトピックが消えたり、ということも時折起こっていました。
これらの問題を解決するべく、2008年12月ごろにMixi::Test::Fixturesという新しいライブラリが実装されました。Mixi::Test::Fixturesは、RailsやDjangoにある「fixture」のしくみを模したライブラリで、具体的には次のように動作します。
- テストの実行直前に一時的なデータベースを作成し、各種コードからの接続先をそちらに向ける
- 指定されたYAMLからデータベースに値を挿入する
- テストを実行する
- テストの実行直後に、最初に作成したデータベースを削除する
当時のmixiでは、SQLのかなりの部分が手作業で書かれていて、O/RマッパはおろかSQL::AbstractやSequelに類するクエリ生成ライブラリも存在していませんでした。そのため、SQL文のかなりの部分がMySQLでしか動かず、また変換するにもフックポイントがありません。結果として、一時的なデータベースにもSQLiteのようなプロセス内で動作する軽量な実装を避け、MySQLをそのまま使っています。
Mixi::Test::Fixturesは、その後も継続的に改良が加えられています。今では、memcachedやメールの送信、外部へのHTTPアクセスなども実際の処理が走らないように変更できます。これらは、SQLが使えるようなRDBMSと比べるととても単純なため、Perlで書かれたプロセス内で動作する実装を作り、指定されればそれに差し替えるようになっています。
テストをすべて実行する
fixtureの導入によって、新しいテストは書きやすく、気軽に実行できるようになりました。一方でソースツリー全体を見渡すと、それ以前に書かれた、実行にいくつかの準備が必要だったり、共有のデータベースを書き換えてしまうようなテストも多く存在していました。
コードを変更したあとには、できる限り多くのテストを実行し、変更の正しさを確かめるべきです。実行しにくいテストを取り除き、「テストが失敗することは問題である」と自信をもって主張できる環境を早急に作るうえで、これらのテストの存在は邪魔になっていました。ただ一方で、これらの中には、事前の準備が必要なことを差し引いても、それなりに有用なものも多くありました。そこで、これらをひと思いに削除することはあきらめ、「ブラックリスト」を作ることにしました。
ブラックリストは、問題のあるテストの集合です。実際には、ソースツリー内にあるテストのうち、問題のあるものを列挙したリストをYAML形式のファイルとして作成しました。問題が解決したら、リストからそのテストを削除していきます。同時に、ブラックリストを使って「実行可能な(ホワイトな)テスト」を列挙するコマンドも作成しました。
始めからホワイトリストを作らなかったのには理由があります。ホワイトリストを作成する場合、
- ①誰でもいつでも実行できるテストを作成した人は、それをリポジトリに追加すると同時に、ホワイトリストにも変更を加える
- ②前準備が必要だったり、自分以外に実行されては困るテストを書いた人は、それを単にリポジトリに追加する
という作業が個々の開発者に要求されます。しかし、開発者としてより望ましい行動は①です。望ましい行動のほうが手間がかかるというのは、正しいしくみとは言えないでしょう。
図2は、導入から現在に至るまでのブラックリストの行数です。340台から一気に減少したあとは、細かな増減を繰り返しながら200台近辺で落ち着いています。テスト全体の件数は増えているので、ソースツリー全体に占める割合は下がり続けているものの、数として下げ止まってしまったのは今後の課題です。
継続的インテグレーション
すべての(実際にはブラックリストに列挙されているものだけを除いた)テストが実行できるようになると、その実行時間が問題になり始めました。遅いテストの終了を待つのは苦痛で、しばしば実行せずに済まされがちです。mixiでも、安定版からのブランチで発見したテストの失敗が、実は安定版の時点で発生していた、ということが時折ありました。すべてのマージごとにすべてのテストを実行すれば、このような問題は防げていたはずです。
そこで導入したのが継続的インテグレーションのしくみです。mixiでは最初にBuildbotを導入し、現在はJenkinsを使っています。Jenkinsは安定版へのコミットを定期的に監視し、変更があった場合にビルドを実行、結果を社内IRCのチャネルに書き込みます。なお、Jenkinsの用語に合わせて「ビルド」という語を使いましたが、mixiの場合の「ビルド」は、単に自動テストの実行を指しています。
mixiの自動テストは非常に遅く、現在、実行に15分程度の時間を要します。そこで、コミット直後には「今のコミットに含まれるテストと、関連するファイルのテスト」だけを実行するしくみを別途用意しています。mixiでは、コードとテストのファイル名に「lib/Mixi/Foo.pmのテストは、t/lib/Mixi/Foo.tあるいはt/lib/Mixi/Foo/以下に存在する」というゆるやかなルールがあるため、関連するファイルもこのルールを使って探しています。
この「今のコミットに含まれるテストと、関連するファイルのテスト」はたいてい30秒もかからずに終了し、これが成功した場合のみ、全体のテストが走り出します。このしくみによって、15分もかかるテストが、コミット時にファイルを追加し忘れた、といったくだらないミスで失敗することを防いでいます。
説明の都合上、まず開発者テスト、次に継続的インテグレーションという順になりましたが、実際には、この2つは並行して導入を進めていました。継続的インテグレーションの存在は、「テストを書いたけど誰も実行してくれない」という問題を軽減します。CやJavaで書かれたソフトウェアなら、ビルドの依存関係の片隅にテストの実行を忍ばせることも可能ですが、スクリプト言語の場合そうはいきません。少なくともmixiの場合はそうでした。また、IRCにテストの失敗を通知し、実際にリリース版でも問題が見つかるという流れは、自動テストの認知向上につながったと思います。
任意のブランチをテストする
mixiでは、trunkを安定版とし、すべての変更をブランチを作って行うルールで運用しているため、リポジトリ上には大量にブランチが存在しています。一方、Jenkinsが定期的にテストを実行しているのは、安定版と、複数の人々が長期で関わる大きなプロジェクトのブランチだけです。その他の多くのブランチでは、普段は自分の開発環境で、自分が変更した部分に関連したテストだけを実行し、必要に応じてJenkins上で全テストを実行する、という運用がされています。
Jenkinsで実行できるテストは、開発環境で実行できるものと変わりません。ただ、開発環境はJenkinsで使っているものと同様のスペックのものを仮想化して分割しているため、あまり速くありません。全テストの実行には、前述の15分よりさらに長い時間がかかってしまいます。
また、1つのVMの負荷が、物理的に同じホストに同居するほかのVMに影響してしまうという問題もあります。Jenkinsのホストならば、エディタやシェルのような対話的なソフトウェアはなく、負荷をあまり気にせずにかけられます。15分かかるテストの実行完了が16分になるのと、エディタの入力が一瞬遅れるのとでは、前者のほうが許容しやすいのではないでしょうか。
Jenkins上で任意のブランチをテストするために、mixiではParameterized Buildと呼ばれる、外部からパラメータをとってジョブを起動するしくみを使っています。まず、パラメータとしてブランチ名をとり、そのチェックアウトからテストまでを実行するジョブを作ります。開発者は、自分のテストしたいブランチをパラメータとして、このジョブを好きなタイミングで起動しています。
Parameterized Buildは、HTTPリクエストからパラメータ付きで起動できます。現在は「make remote-test」と打ち込むだけで、現在のワーキングコピーに対応するブランチのテストが、Jenkins上で実行されるようになっています。
まとめ
今回はmixiの自動テスト周辺の歩みについて、順を追って説明しました。
冒頭で述べたとおり、筆者は現在、こうした開発の裏側の改善をおもな業務としています。ただ、自動テストの導入から継続的インテグレーションの途中あたりまでは、お客様が直接目にするようなフロントエンドのアプリケーションの開発にも関わっていました。テストまわりの改善は、同じような不満を抱えた同僚と協力しながら行う、ボトムアップの仕事でした。
みなさんの中にも(少ないことを祈りますが)、大きく、テストの手薄なコードベースを相手に、日夜格闘している方がいるかもしれません。高速な単体テストが無理なら、まず人の手を介さない自動テストから、差し替え用のモックオブジェクトを書く時間がないなら、まずデータベースの接続先を変えるところからと、徐々に歩みを進めていくことは可能です。今回の記事がそうした方々の一助になれば幸いです。