ノーフレームワークのレガシーPHPがCIに乗るまで

ついに仕事で触っている PHP のコードがほんの一部のテストとは言え CI に乗った

正直これは感動ものだ。

今回はここに至るまでの長大な物語をダイジェストでお届けしようと思う。

有史以前

  • PHP 3 で作られた 1 URI : 1 スクリプト + 共通関数 時代
    • 当然のように PHP と HTML と SQL 混在
  • まともなテスト環境がなかったので似た環境をどうにか作る
  • パスとか絶対で埋め込みまくりなのでとりあえず共通のパス情報の変数に差し替えまくり
  • テスト環境用のコードと本番環境用のコードが違う

オール目視

つらかった。

みなさんの予想通りバージョン管理なんてものは存在しなかった。

素朴なPHPを徐々にclassに

  • class になれば phpdoc を書きやすくなる
  • いきなり実行しないようにすればテストしやすくなる
    • これは後から気づいたんだけど、結局フロントはロクに自動テストできてない

一時期 phpdoc に夢中になっていた。今も大事に思ってはいるが、完全に独立したライブラリのドキュメント以外には助けられた感じはあまりしない。

とりあえずテンプレートを導入してHTMLを分離

当時はテンプレートにはロジックは入ってはならないと頑なに信じていたんだけど、おかげでテンプレートを制御するコードが妙に複雑になることに徐々に気づき、Rails の View を erb で書いて以降は

結局 PHP の <?php ?> 方式は適切なスコープの分離ができないことが問題なだけで、そんなに悪くなかったんだなぁ

と思っている。

ただ PHP の <?php ?> 方式はあまりに問答無用でかなりダメだと思うけど。

とりあえずCLIで叩き回る&目視

PHP 5 への移行時に苦し紛れで使ってた方法。今の capybara のようなツールを知らなかったのでひたすら PHP の cli バイナリで実行しまくった。

  • とりあえずすべてのフロントの PHP を find で洗い出して、cd して1叩きまくる2
  • これでも致命的なエラーはだいたい洗い出せるので、これを毎日 cron で回して通知させて片っ端から直した

しかしテンプレートオブジェクトが clone されたかどうかがキモになる部分がいくつもあり3、最後は目力頼みに。

今ならなんとかこれもテストできるだろうなぁ。

本番サーバの監視ツールを自作

頑張って自動で叩いたり目視したりしてたけど、やっぱ網羅はできなくて、どうしても本番サーバで問題が発覚したりした。仕方ないのでログを監視することにした。と言ってもそんなにたいしたものではなく、Webサーバの error log を監視して PHP の E_ERROR とか E_WARNING を拾って通知するだけ。

Rswatch なんてものを書いてみました

要するに拡張性の乏しい Swatch を Ruby で再発明したということです。今ならもっと賢いツールがたくさんありそう。当時は標準のパッケージで入らないものはできるだけ避けたかったのと、環境が古いので自作した。PHP 5 への移行期に 4 でも 5 でも動くようにしながら、本番環境の 4 で問題が起きたかどうかを早く知るために書いたものであり、想定している Ruby のバージョンは 1.6.8 である。すごい。

書けるところからユニットテストを

実は少し PHPUnit でテストを書いていた時期がある。しかし PHP 5 への移行を考慮して割とすぐに SimpleTest にした。当時は PHPUnit が PHP 4 用で PHPUnit 2 が PHP 5 用と分離していたのに対して SimpleTest は両対応で、テストコードを資産として残しやすいと判断したため。

以来、結局 SimpleTest を使い続けている。

今もメンテは続いており、ややマイナーではあるもののなかなか良いツールだと思う。

現在のコードは徐々になんちゃって MVC になっており4、少なくとも M の部分とヘルパーやユーティリティ的な部分は独立してテストできるので、だいぶマシになっている。

正直どんな機能にするかよりもどうやったらテストできるか、どうやったらオブジェクトの独立性を確保できるかをいちばん考えているかもしんない。

これは以前も書いたけど要するにリファクタリング及びコードの変更をやりやすくするため。どうせ自分は失敗するから。

TestCaseからTestSuiteへ

目の前の課題にいっぱいいっぱいだったのと、当時 SimpleTest には auto runner もなく、余計なコードはできるだけ書きたくないのですべての TestCase を手で叩く仕様にしていた。本当に余裕なかったんだなぁ。

これがのちのち効いてくる。TestSuite にしようにも require した瞬間にテストが走るのでまとめることができない。しばらくして production のコードと同じく

if ( realpath( $_SERVER['SCRIPT_FILENAME'] ) == __FILE__ ) {

}

でテストの実行を囲んで、今は TestSuite を叩くとすべてのテストが自動で走るし、個別にも実行できるようになっている。

auto runner をちゃんと使うとまた違うのかも。まだうまく試せてない。PHP のバージョンを上げることができたら A continuous test runner for CLI v3 - Stagehand_TestRunner - Piece Framework を導入して楽したい。

end-to-endテストはRubyでやろうとしている

PHP にも Behat という Cucumber inspired なツールがあるんだけど、これは PHP 5.3 以降を要求するという、なかなかにハードなシロモノである。

まぁ CI サーバを 5.3 で組んだので CI サーバ上では使えるんだけど、CI でしか動かないのは面倒だし、すでに RSpec は使えるので RSpec + capybara-mechanize でいいじゃんと思っている。

ちなみに Cucumber 系はちょっと準備が面倒なのと、いちばんの理由はシナリオファイルはどんな言語でもないのでエディタのサポートが弱いという点で導入には躊躇している。テストの書き方をミスった場合にその発見に時間が掛かりそうなので。

まだこれでイケそうという手応えをつかんでいるだけで、具体的にはそんなに作業は進んでいない。

end-to-end のテストが整備できたら、今の中身はバッサリ捨てたいなと思っている。

※ ちなみに、完全に自動化するのを諦めて実装時の反復だけとにかく速くするために Selenium は導入していた。最近はあんまり使ってないけど5

結論

ゼロから作れるならとにかくデキのいいフレームワーク使え。

特にテスト支援は安心を、DBのmigrationは勇気を与えてくれるので重視した方がいい。

CI も最初から導入しろ。テストなしに繊細なコードを書くことに比べれば Jenkins の導入は信じられないくらい簡単だ。簡単で効果の大きいことから始めた方がいい。

自分がスーパーハッカーでもっとずっとプログラミングできる立場なら、もっとずっと早くなんとかなったんだろうなぁ。

  1. なぜならブラウザからアクセスしたときはスクリプトの場所が cwd になるのが PHP の仕様だから、これをアテにしまくっているのだ 

  2. 実際にはややこしい例外がいくつもあったので YAML で除外対象を定義できる Ruby スクリプトを用意してそれで回した。 

  3. PHP は 4 から 5 になる段階で、それまで代入時にデフォルトでコピーされていたオブジェクトが clone で明示しないとコピーされないように変更された。この影響が思いのほか大きかった。しかもこの変更はオブジェクトに対してだけで、built-in の array などはそのままなのでかえって混乱のもとになっていると個人的には感じている。 

  4. 間にとりあえずフロントコントローラ方式のオレフレームワークを組んでみたりした。これは当時増えていたフレームワークがほぼ PHP 4.3 以降を要求するのに対し、利用していた環境が 4.2 だったから。この環境の制約とそれを乗り越えるためのオレライブラリ、オレフレームワークの実装はとても勉強になった。どれくらい理解していたかと問われると甚だあやしいが、Rails や ZendFramework を参考にしていた。 

  5. 不安になるのは必要な情報が正しく伝わるかどうかなのに JavaScript だけでは Model の状態が分からず、どーも内部の変更の影響を受けやすい。今の自分の力では fragile test になりやすい。わざわざ rc から PHP で〜みたいなことを考えるのも割に合わない感じなので諦めちゃってる。どうしても必要なときだけ Selenium IDE から HTML 吐き出して使い回したりする。 

More