Time to Read

5分

皆さん、楽しくテスト書いてますか~~~

今日は、 rack-legacy を使うことで、 PHP のアプリケーションなのになぜか Ruby のコードでテストできて(受け入れテストのあたりですけど)、そうすると本物の RSpec とかが使えて楽ですよ、と言う話をします。

rack-legacy とは。

まず、 Rack とは、 Ruby 製のウェブアプリケーション(Ruby on Rails、Sinatraなどをご存知かもしれません)とサーバ(Apache/Passenger、NginX、Lighttpdなどなど…)をつなぐ一種の規格です。 Perl や Python にも似たような仕組みがあります。

ここで、 Rack::Legacy というミドルウェアを使うと、他の言語の CGI や、 PHP のアプリケーションが Rack の上で動かせます。

そして、 Rack の上で動くということは、つまり Ruby の他のテストツール一式が使えるようになる、ということです。 Ruby を書く人は概ねテストの自動化のことばかり考えているので、おかげで Ruby のテストツールは大変豊富でパワフルです。その恩恵を PHP のアプリで受けられるかもしれません。むろん、 PHP も Ruby もこんな用途で使われるとは誰も思っていないですし、以下のコードは無保証です。

なお、なぜ「Legacy」という言葉がライブラリの名前に入っているかというと、これはいわゆる Rubyist ジョークというやつで、ツンデレに近い何かです。決して、 PHP がレガシーである、ということでは無いと思います。ただ、僕はこのライブラリの作者では無いので詳しい意図は不明です。

PHP と Ruby の準備をする。

まず PHP 側をふつうに動かす環境を用意してください。 rack-legacy では内部で、 php-cgi を使います。 Debian 系なら、

1
sudo apt-get install php5-cgi

と言うコマンドでインストールできます。他の Linux でも何とかなるでしょう。 OSX でも多分大丈夫でしょう。 Windows … は僕は持ってないのでよく分かんないっす><

Ruby 側は、 1.9.2 以降を推奨します。

1
sudo apt-get install ruby1.9.2

もしくは素直にビルドする、 RVM をこの際だから入れてみる、など色々手はありますでしょう。

アプリの土台をつくる。

以下のような構成でファイルとディレクトリを作成しておいてください。

1
2
3
4
5
6
7
8
/path/to/phpapp
    ├── Gemfile
    ├── app.rb
    ├── config.ru
    ├── public
    │   └── index.php
    └── spec
        └── php_app_spec.rb

public と言うディレクトリ以下に PHP のアプリケーションを突っ込む予定です。

まず、 Gemfile を以下のように作成。

1
2
3
4
5
6
7
8
9
10
source :rubygems
 
gem 'rack'
gem 'rack-legacy'
gem 'rack-rewrite'
 
group :test do
  gem 'rspec'
  gem 'rack-test', :require => 'rack/test'
end

そうしたら、以下のようなコマンドで必要なライブラリ(Ruby業界では gems と言います)を入れます。Ruby 業界では、なぜかバージョン1.9.2を入れていても接尾辞として1.9.1が付く場合がありますが、そういうもんだと思ってください。

1
2
sudo gem1.9.1 install bundler
cd /path/to/phpapp && sudo bundle

時間が掛かりますが、多くの Rubyist が普通に「bundle install 時間掛かりすぎなんだよ!!!」と思っていますので、きっと誰かが改善してくれるとおもいます。

そして、その他の必要ファイルを以下のように作成します。

config.ru :

1
2
require File.expand_path('../app', __FILE__)
run TestPHP

app.rb :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require 'rubygems'
require 'bundler'
Bundler.require
 
public_dir = File.expand_path('../public', __FILE__)
 
TestPHP = Rack::Builder.app do
  # .htaccess の DirectryIndex index.php の代わり
  use Rack::Rewrite do
    rewrite %r'(.*)/?', '$1/index.php', :if => lambda{|e|
      File.directory?(File.join(public_dir, e['PATH_INFO']))
    }
  end
  use Rack::Legacy::Php, public_dir
  run Rack::File.new(public_dir)
end

Ruby に詳しくない人は、まあおまじないみたいなもんだと思ってください。

TDD ことはじめ。

いよいよ、テスト(スペック)ファイルを作成します。まずは、「”/index.php” にアクセスすると”Hello, PHP”と表示する」ことを実装してみましょう。

以下のような spec/php_app_spec.rb を作ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# coding: utf-8
ENV['RACK_ENV'] = 'test'
 
require File.expand_path('../../app', __FILE__)
Bundler.require :test
 
def app
  TestPHP
end
 
RSpec.configure do |conf|
  conf.include Rack::Test::Methods
end
 
describe 'PHP アプリケーションについて、' do
  it '/index.php にアクセスできる' do
    get '/index.php'
    last_response.should be_ok
  end
 
  it '/index.php にアクセスすると「Hello, PHP」を表示する' do
    get '/index.php'
    last_response.body.should match /Hello, PHP/
  end
end

describe~より上の行は、かなり Ruby 語な感じです。これもおまじないみたいなもんと考えて構わないと思います。

大事なのは、 describe のブロックに囲まれたゾーンで、

  • /index.php にアクセスできること
  •  → last_response.should be_ok に対応
  • /index.php にアクセスすると、「Hello, PHP」を表示すること
  •  → last_response.body.should match /Hello, PHP/ に対応

の二点が満たされるように、これから本体のコードを書いていこう、ということを表現しています。このように、「どうすれば」「どうなるか」をなるべく自然言語に近い形(英語ですね)で表現できて、それが実はそのまま Ruby として正しいコードになっている、と言うところが RSpec の RSpec らしい点なのです。

まずは、テストを動かします。 -fs は結果表示フォーマット、 --color は色付きにします。

1
rspec -fs --color spec/*.rb

すると、以下のように表示されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PHP アプリケーションについて、
  /index.php にアクセスできる (FAILED - 1)
  /index.php にアクセスすると「Hello, PHP」を表示する (FAILED - 2)
 
Failures:
 
  1) PHP アプリケーションについて、 /index.php にアクセスできる
     Failure/Error: last_response.should be_ok
       expected ok? to return true, got false
     # ./spec/php_app_spec.rb:18:in `block (2 levels) in &lt;top (required)&gt;'
 
  2) PHP アプリケーションについて、 /index.php にアクセスすると「Hello, PHP」を表示する
     Failure/Error: last_response.body.should match /Hello, PHP/
       expected "File not found: /index.php\n" to match /Hello, PHP/
     # ./spec/php_app_spec.rb:23:in `block (2 levels) in &lt;top (required)&gt;'
 
Finished in 0.03376 seconds
2 examples, 2 failures

これは、テストが正しく動いて、 正しく失敗した ことを示します。これは何もコードを書いていないので当然ですね。

では、試しに、空の index.php を作ってみましょう。

1
touch public/index.php

これで、

テストが一つ通過しました!

さらに、さきほどの public/index.php を以下のようにしてみます。

1
2
3
<?php
  print 'Hello, PHP!' . "<br />\n";
?>

これで、以下すべて通過しました。

  • /index.php にアクセスできる
  • /index.php にアクセスすると「Hello, PHP」を表示する

以上二点が実装できたということになります。

次に、「/index.php?name=Rubyist」にアクセスすると「Hello, Rubyist」と表示する、と言う機能を追加する場合はどうなるでしょう。以下の記述を、 describe の中に追記してみます。

1
2
3
4
  it '/index.php?name=Rubyist にアクセスすると「Hello, Rubyist」を表示する' do
    get '/index.php?name=Rubyist'
    last_response.body.should =~ /Hello, Rubyist/
  end

index.php 側で実装しましょう。

1
2
3
4
5
6
7
<?php
  if ($_GET['name']) { 
    print 'Hello, ' . $_GET['name'] . "<br />\n";
  } else {
    print 'Hello, PHP!' . "<br />\n";
  }
?>

  • /index.php?name=Rubyist にアクセスすると「Hello, Rubyist」を表示する

も実装完了しました。以上のようなリズムが TDD の基本だとおもいます。 Let’s enjoy testing!!

まとめ

  • RSpec その他 Ruby のテスティングツールを、頑張ればある程度 PHP に適用できる
  • RSpec は、「望ましい動き」をほぼそのままコードに落とせるので、書くのが楽(ただし、好みの問題もあるでしょう)

* * *

まあ、このエントリは壮大な釣りのようにも見えますが、たとえば Capybara などを PHP アプリケ~ションに使えたりとかその手の変態行為が出来るはずなので、ただのネタ以上に使える可能性も否定できません。でも、釣りの要素があることは否定できません。

普通、きっと PHPUnit とか 「->」がかっこいい PHPSpec とかを頑張って使うべきなんでしょうが、でも、もしそもそもあなたが Ruby に慣れていて、でも受け入れテストなどを「サックリ RSpec とかで書きたいでござる」みたいな要望を満たすために、上記のような技が使えるかもしれません。使えないかもしれません(.htaccess を Rack::Rewrite のDSLに書き換える手間とか、$_SERVER をはじめとする環境系変数とかが大変怪しいです)。

なお、 rack-legacy 自体は IO.popen でファイルを開いて実行して 、その出力を利用しています。 ここは笑うところです!  そりゃ動くけどさあ……。

この記事を読んで Ruby に興味を持った方は:

あとは素直に本を買いましょう~~~、でも、一番いいのは何か作ってみましょう。

初めてのRuby

著者/訳者:Yugui

出版社:オライリージャパン( 2008-06-26 )

大型本 ( 224 ページ )


メタプログラミングRuby

著者/訳者:Paolo Perrotta

出版社:アスキー・メディアワークス( 2010-08-28 )

大型本 ( 312 ページ )


この記事を読んで TDD/BDD/受け入れテスト自動化 に興味を持った方は:

こっちも追記しました。 @ryuzee さんが大変良い記事を書いていらっしゃいますので、普通に PHP でも頑張れるとおもいます。

併せて読みたい(手動)