Pinto公開に向けて #18 ― RSpecのモックを使ってみた
あらすじ
Pintoというソーシャルブックマークサービス、およびPintoBeansというWebアプリケーションフレームワークをRubyで開発中です。開発中に気づいたことや工夫した点などを、備忘録も兼ねて書いています。その18回目です。
Rackインターフェーサが必要だ
Rack自体、Webサーバへの依存性を排除するためのインターフェーサなわけですが、石橋を叩いて破壊する性格の私は、Rackにすら依存したくないので、さらにインターフェーサを書く必要があると感じました。
RackはHTTPリクエストをHashに変換し、Rubyスクリプトのエントリポイントに渡してくれます。そのエントリポイントで、Hashを内部用のリクエストオブジェクトに変換するイメージです。Rackへの戻り値は、内部用レスポンスオブジェクトをRack用の配列に変換したものとなります。
エントリポイントではまた、内部用のリクエストオブジェクトをルータに渡し、処理を委譲する必要もあります。なお、ルータ以下はRackの実装に依存しません。これが所期の目的なわけです。
エントリポイントの実装はこんな感じ
module PintoBeans class RackServer def call(env) request = PintoBeans::RackInterfacer.env_to_request(env) response = PintoBeans::Router.route(request) PintoBeans::RackInterfacer.response_to_array(response) end end end
で、このメソッドのテストをどう書けばよいのか、しばらく悩みました。
そもそも何をテストするのか?
しばらく悩んだ結果、テストの内容は:
- ルータに正しいリクエストオブジェクトが渡されるか
- ルーティング処理が1度だけ実行されるか
- 戻り値が「ルータからの戻り値をRack用の配列に変換したもの」であるか
であろうと結論づけました。エントリポイント自身はルータ処理の詳細を知らなくてよいでしょう。
テストをどう書くか
ルータをモックにするためには、コンストラクタやセッタでDIする手があります。が、Rubyならばテスティングフレームワークでどうにでもできるはずです。私はRSpecを使っているので、RSpecのモック機構を使ってみようと思いました。
で、できたテストコードが下記のものです。
$LOAD_PATH << 'lib' require 'pinto_beans' describe 'PintoBeans::RackServer.call' do it "should: 1. transform request, 2. call PintoBeans::Router.route with it, 3. transform router's response, 4. and return it" do mock_response = PintoBeans::HttpResponse.new mock_response.status_code = 200 mock_response.headers = {'Content-Type' => 'text/plain'} mock_response.body = 'dummy' PintoBeans::Router.should_receive(:route) do |request| request.uri.should == 'http://pinto.jp/dummy' mock_response end.once env = Rack::MockRequest.env_for('http://pinto.jp/dummy') response = PintoBeans::RackServer.new.call(env) response[0].should == 200 response[1].should == {'Content-Type' => 'text/plain'} response[2].should == 'dummy' end end
前掲の3つのテスト内容は網羅できていると思います。Mochaならば、もう少しきれいに書けるのかもしれません。そのうち試そうと思います。
ちょっとした悩み
テストであっても、モックが必要になるならば、DIするのが筋のような気がしています。例えば:
module PintoBeans class RackServer attr_accessor :router def call(env) request = PintoBeans::RackInterfacer.env_to_request(env) response = @router.route(request) PintoBeans::RackInterfacer.response_to_array(response) end end end
のような感じですね。
今回のケースはRackインターフェースなので他言語への移植は考えにくいですが、Rubyのオープンクラスに頼りきるのではなく、モックはできるだけDIする、という考え方もありなのではないかと思っています。どうなんでしょう?