岩本隆史の日記帳(アーカイブ)

はてなダイアリーのサービス終了をうけて移行したものです。更新はしません。

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する、という考え方もありなのではないかと思っています。どうなんでしょう?