2012-04-17

ノーフレームワークレガシーPHPから始めるユニットテスト

巨大なアクセスを抱える大規模サイトは経験ないので分かりません。

あと、本当にレガシーな PHP しか考えてません。とにかくイマドキのフレームワークを使ってください。こんなこと気にする必要ないんでしょ?

設定

できるだけphp.iniに依存しない

  • まず、PHP のコードで設定できるものは PHP のコードで設定した方がよい
  • そうすれば複数サイトの開発のための切り替えも比較的スムーズに行える
  • extension など php.ini にしか書けないものは php.ini に書けばよい

これを踏まえたうえで、自分の場合はプロジェクトごとに .htaccess および php.ini を同時に生成する setup 用のスクリプトを用意している。.htaccess に書く rewrite の設定などもここで展開するようにしている。で、このスクリプトをリポジトリに入れてある。

必要に応じて RewriteBase も設定できるようにしておけば、ある程度は開発環境と本番環境でパスが違うといった状態でも開発可能になる。1

こうしてプロジェクト、環境ごとの違いを setup スクリプトに与えるパラメータだけで吸収する。これのおかげで mod_php か cli かを気にせずテストできるようにしている。

今回 Jenkins を入れる際にもこの setup スクリプトのおかげで実は設定自体はすぐに済んだ。

PHPでPHPを設定する

この話はすでに PHP の設定って PHP で書いた方がよくない? に書いてある。

PHP 5.3 からは php.ini 内で path や host でセクション指定して設定を分けることができるんだけど、システムの php.ini ってプロジェクトのリポジトリには馴染まないので、別メンテになってしまってよろしくない。

iniファイル + auto_prepend_file

プロジェクトごとの ini ファイルを用意するとテストは

php -c inifile -f testsuite.php

のように実行することができる。

この inifile の中で

include_path =
auto_prepend_file =

を指定してやれば

  • 共通のライブラリのパスを設定
  • 設定用の PHP の読み込み

を行える。

実際には自分の場合は

ROOT
  .htaccess
  _test/
    .htaccess
    php.my.ini
    testing.php
  app/
  config.php
  php.my.ini
  ...

みたいな感じになってて、testing.php から config.php を読み込むようにしている。

  • 普段のアプリの実行は config.php のみで
  • テストのときはテスト用のライブラリの読み込みとか準備を testing.php で行いつつ config.php も読み込む

という流れにしてある。

まぁ testing.php は spec_helper.rb みたいなもん(ひどい)

ユニットの切り出し方

『レガシーコード改善ガイド』読め

まぁこれだけだとなんなので実際いちばんよくやったのだけ紹介。

とりあえずclassに

もう

<?php
class Klass {
  function run() {
    最初からあったコード全部
  }
}

if ( realpath( $_SERVER['SCRIPT_FILENAME'] ) == __FILE__ ) {
  $app = new Klass();
  $app->run();
}

こんなんでもよい。

いきなり順次実行さえしなければあとで必要なときに料理できる。

まさかのグローバル変数はstaticなclassに

PHP 3 の時代は require してる他のファイルのグローバル変数を前提にしているものもままあったけど、さすがに今はない…よね?

どうしてもって場合は static な class 用意してメソッド経由でメンバ変数読み書きさせればマシかな。そうすりゃこの class にログの仕組み仕込めば追跡できるし。

確認したい部分をメソッドに

メソッドに切り出す段階でグローバル変数は global 宣言が必要になるので、ついでにメンバ変数にしちゃう。

で、切り出したメソッドはできるだけシンプルな入出力を提供する形にする。ダメならメンバ変数の状態でたぶん何かが確認できるので、それをテストする感じになると思う。

重たいコンストラクタはバラす

DI だろ DI (愛だろ、愛の感じで) とは思うけど、すぐにできない場合は

class Klass {
  function __construct() {
    ...
    $this->_init_foo( $foo );
    $this->_init_bar( $bar );
    ...
  }

  function _init_foo( $foo ) {
  }

  function _init_bar( $bar ) {
  }
}

こんなんでよい。で、テストのときは

class Klass_Test extends UnitTestCase {  // SimpleTest の場合
  function setUp() {
    $this->obj = new Klass();
    $this->obj->_init_foo( $config );
  }
}

みたいな感じでテストに都合のいい設定やオブジェクトを突っ込む。

ちなみにテスト用 class の名前を

Klass_Test

にしたのは理由があって、Klass を継承してテスト用に変更するときに

TestingKlass

という名前を使うから。これは『レガシーコード改善ガイド』の中で紹介されている命名ルールに従っている。

スプラウトクラス

レガシーコード改善ガイドに出てくるテクニックのうち、いちばん好きなもの。

でかくて複雑なメソッドを丸ごとクラスに追い出してそのクラスの中でバラしていく方法。

なんだけど、

とにかくまっさらでテストしやすいclassを作る

ためにも使える。

本買って読んで!

ライブラリにする

テストしやすさのためには要するに独立していることが大事なので、当然の流れとしてライブラリ化というのは出てくると思う。

以下脱線。

気をつけないとこのライブラリこそが癌になりかねない。このソーシャルコーディングの時代に自社で閉じたライブラリを整備することは十分な切り分けを考えた上でやらないといけないと思う。

個人的には PHP は LL の中では使いやすい大きさのちょうどよいライブラリがとても少ないと思う2ので、勢いこうしたライブラリの整備に話は進みやすいと考えている。本当にレガシーな環境(PHP 4とか)であれば仕方ないけど、そうでないならオリジナルライブラリはできるだけ最小限に留めた方がよいと思う。

  1. パスに関しては本当は PHP 5.4.0 以降の built-in HTTP サーバなど、他の LL のようなサクッと起動できる開発用のサーバがあれば話は簡単なんだけど。 

  2. なぜかみんなフレームワークの中に収めようとする 

About

例によって個人のなんちゃらです