「CIを半年間まわしてみて」というお題でLTをしてきました

大分時間も経ってしまい今更ではありますが、先日行われた第67回 PHP勉強会で「CIを半年間まわしてみて」というお題でLTをしてきました。

昨年の11/30に、当時ちょうど開発が始まった案件の開発環境に関して「今時なCakePHPでの開発環境!?」というエントリーを書いて、初のホッテントリ入りしました。4月末でこのプトジェクトが始まって半年という事で、実際にCIをまわしている中で起こった事や、試行錯誤しつつどうやって解決したかなどを簡単にまとめてお話ししました。

LT用に作った資料ではちょっと伝わりにくいので、以下にまとめ直しました。

成長の軌跡

Jenkinsサーバーを立ち上げた時は、UnitTestのテストケースが10個だけだったのですが、4/30現在 UnitTestのテストケースが467件、受入れテストのシナリオ数が292件とものすごい成長っぷりです。

f:id:kaz_29:20130430122241p:plain

f:id:kaz_29:20130430120730p:plain

この半年間に起こった事

テストコードはいつ書くか?

今回のプロジェクトはUnitTestやBDD Plugin(BeHat)を使用しての受入れテストは書きましたが、TDD/BDDで開発していた訳ではありません。私自身もプロダクションのコードを書きながらテストコードを書いて、一通りブラウザ上で動作する所まで進んでから、受入れテストを書いていました。

年末〜年始にかけて手伝ってくれるメンバーが3名増えたのですが、実際にコードを書き始めて起こったのが...

プロダクションコードとテストコードが一緒にコミットされない!

折角CIしているのにこれでは全く意味がありません。という事で「テストコードが無いコードはmasterにマージしません!」と宣言して実際にマージしませんでした。masterにマージされなければ当然ステージング環境にも反映されませんので、ストーリ(チケット)が完了しません。という事で(おそらく(^^;)渋々テストコードを書いてくれたんではないかと思います。

自動テストに40分かかる

開発も順調に進み、テストコードが増えてくると皆さん直面するスローテスト問題が発生しました。当初は、インスペクション(静的解析)/UnitTest/BDD(StoryTest)を順番に実行していたため、各処理を分割して、インスペクションとUnitTestは並列に実行する様にしました。また、ビルドパイプライン化してUnitTestが正常に完了した場合のみStoryTestを実行する様に変更しました。この変更ですべてが完了するのに20分台まで時間が短縮しました。

ステージングへの自動デプロイがエラー

今回のプロジェクトではビルドパイプラインを作って、masterブランチで受入れテストが正常に完了すると自動的にステージング環境にデプロイされる様になっています。もちろん、テーブルの変更/追加時も自動でデプロイする為に、Migrations Pluginを使って自動でマイグレーションしています。そして、自動デプロイの際にこのマイグレーションに失敗してしまいました...。

原因は単純なミスだったのですが、ステージング環境のDBがつじつまの合わない中途半端な状態になってしまいクライアントさんにごめんなさいして、再度構成し直しました。これがプロダクション環境で起きたら...と思うと冷や汗がでました。

では、なぜこんな問題が起こったかというと、Jenkins上のUnitTestやBDD環境でテストを実行する際のマイグレーション手順がステージング環境と違っていたためでした。

この辺りにも書かれている通り、原則なはずの「すべての環境で、同じ手順でデプロイするべし」をしっかり守る事で、開発環境やテスト環境などで繰り返しデプロイ(この場合はマイグレーションですが...)が実行され、問題がある場合にも事前に発見出来る様になりました。

selenium-standalone-serverが落ちる

githubのcommit時に自動でテストが実行されるので、CIサーバーはもちろん24時間動作しています。継続して運用をしていると、突然ストーリテストに失敗する様になってしまいました。原因を調査すると、selenium-standalone-serverが落ちていたり、メモリ不足が発生していました。メモリーリークが発生しているのだろうと予想して、ストーリテスト実行時に自動的にselenium-standalone-serverを再起動する様に設定したところ改善しました。

ストーリーテストで時々エラーになる

単独で実行したり、開発環境で実行すると問題なく動作するのにある特定のテストケースが時々エラーになるという謎現象が発生しました。色々と調べた結果原因は簡単で、とあるモデルのアソシエーション先のorderが指定されていないため、環境/状況により並び順が変わってうことが原因でした。

アソシエーション先のデータの並び順は通常の処理では問題にならない事もあるので、注意が必要かもしれません。

スローテスト再び...

ビルドパイプライン化して、30分を切っていたストーリーテストがまた40分程度かかる様になってしまいました。色々と悩んだ結果、ストーリーテストを2つに分割して、同時に実行する様に修正しました。今回利用しているBDD Plugin(BeHat)ではシナリオにタグを振る事ができるので、以下の様にJavaScriptを使っていてSeleniumServerを利用するシナリオと、それ以外のシナリオに分割しました。実際にタグで実行するストーリーを切り替えるには以下の様に実行します。

$ ./app/Console/cake Bdd.story --tags=javascript // javascriptタグが付いているもの
$ ./app/Console/cake Bdd.story --tags=~javascript // javascriptタグが付いていないもの


複数のタグを付ける事も出来るので、機能毎にタグを付けておくとテストの実行範囲を絞り込むことが出来てとても便利です。この修正で、実行時間はほぼ半分の20分程度まで改善しました。

UnitTestがセグフォ

通常のUnitTestを実行する分には、全件テストを実行しても256M程度割り当てていれば問題ないのですが、CodeCoverageを取得しようとすると、膨大なメモリが必要で、プロジェクト開始から半年経過した4月にはとうとう2G割り当ててもセグフォで落ちる様になってしまいました。CI用サーバーのメモリは4Gと立ち上げ当初は多めのものを用意したつもりだったのですが、厳しくなってきました。

試行錯誤した結果、全件一括のテストを諦めてControllerのテストとそれ以外のテストの2つに分割して順番に実行する様にしました。カバレッジ自体は、Jenkinsが複数の結果をまとめてくれるのですが、詳細なhtmlレポートは今のところ統合出来ていません。ちょっと不便なのでなんらか改善策を打ちたいなと思っています。

テストケースが増えてくるとこの問題は必ず打ち当たる問題だと思います。皆さんがどんな風に解決しているかとても興味のあるところです!

またしてもスローテスト…

プロジェクトも終盤にはいって、JavaScriptをガンガン使う大きめの機能が複数完成したのに伴ってまたしてもストーリーテストの実行時間が30分を超えて、40分に迫りつつあります。これに関してはまだ対策は実施していません。4月末で、ひとまず開発作業にめどがつくので、時間を見つつテスト用のデータベースをインメモリ化してみようと思っています。

今回のCIサーバーは、さくらのVPS 4Gを使用しているのですが、UnitTestにしても、ストーリーテストにしても、Fixtureなどを利用してデータベースへの操作が多いテストを実行するとIO負荷が高くなりがちです。VPSにしてもIaaSにしてもHDDのアクセス性能はオンプレミスのサーバーに比べてかなりおそいので、ここがボトルネックになる事がありそうです。最近はSSDを使えるサービスなどもある様なので、色々と試してみたいと思います。

まとめ

今回クライアントさんの理解のおかげで、開発初期からCI環境を用意して進める事が出来、とても貴重な経験を出来ました。CI(continuous integration)は、日本語では継続的インテグレーションと訳されます。半年間継続してきて実感するのは、まさに継続していく事が肝だなと思いました。

今後、このプロジェクトに限らずプロジェクトが進んでいく中で、色々と厳しい状況が発生する事があると思いますが、今回の取り組みを継続していきたいと思っています。