読者です 読者をやめる 読者になる 読者になる

SinatraでModuler Application書いたらテストでハマった

仕事で初めてまともにSinatra使って少しハマったのでメモ。

Sinatraでアプリ書く場合にはClassic StyleとModuler Applicationていう2種類がある。よく見るサンプルなんかは大抵Classic Styleで書かれているものが多いかと。

Sinatra::Base - Middleware, Libraries, and Modular Apps

今回、Moduler Applicationていうタイプでコードを書いてたんだけどテスト書いててハマった。
というのも Testing Sinatra with Rack::Test を見ながら書いてたんだけど、どうにもテストがうまく動かない。

上記ページに載ってるサンプルのうちRack::Testとrspecを使った場合は次のような感じ。

以下のサンプルアプリに対して、

require 'sinatra'

get '/' do
  "Hello World #{params[:name]}".strip
end


テストコードのサンプルはこんな感じ。

ENV['RACK_ENV'] = 'test'

require './hello_world' 
require 'rspec'
require 'rack/test'

describe 'The HelloWorld App' do
  include Rack::Test::Methods

  def app
    Sinatra::Application
  end

  it "says hello" do
    get '/'
    expect(last_response).to be_ok
    expect(last_response.body).to eq('Hello World')
  end
end

このテストはもちろんそのまま動く。
問題はこのサンプルはClassic Styleで書かれているってことで、自分のコードではModule Applicationとして書いていた。
例えば、同内容をModuler Applicationとして書き直すとこんな感じになる。

require 'sinatra/base'

class App < Sinatra::Base
  get '/' do
    "Hello World #{params[:name]}".strip
  end
end

大きな違いはrequireしているのがsinatra/baseになる。
そしてこの状態になると先ほどのテストコードは動かなくなる。
何も修正せずに上記のテストを実行するとこんな感じになるはず。

The HelloWorld App
  says hello (FAILED - 1)

Failures:

  1) The HelloWorld App says hello
     Failure/Error: expect(last_response).to be_ok
       expected ok? to return true, got false
     # ./hello_world_spec.rb:16:in `block (2 levels) in <top (required)>'

Finished in 0.03446 seconds
1 example, 1 failure

Failed examples:

rspec ./hello_world_spec.rb:14 # The HelloWorld App says hello

結論から言ってしまうとModulerスタイルで書いた場合はRack::Test使うときに必要になるappというメソッド内で定義しているSinatra::Applicationを自分のアプリのクラス名にする必要があるってこと。

つまりこうする必要がある。

ENV['RACK_ENV'] = 'test'

require './hello_world' 
require 'rspec'
require 'rack/test'

describe 'The HelloWorld App' do
  include Rack::Test::Methods

  def app
    HelloWorld #=> Sinatra::Application ではなく自分のアプリのクラス名
  end

  it "says hello" do
    get '/'
    expect(last_response).to be_ok
    expect(last_response.body).to eq('Hello World')
  end
end

なんでこうなるかを簡単に言うと、Classic Styleで require 'sinatra' すると実はsinnatra.rb内では上述のsinatra/baseが呼ばれていて、実体としてはSinatra::ApplicationというModuler Applicationが定義されてる。
つまり、Classic StyleとはModuler Application + α だということ。ここのαの部分にbuilt-in serverなんかが定義されてたり。

というわけでModuler Applicationの場合はSinatra::Applicationではなく自分のクラスを使いましょうということ。
ちなみにModuler Applicationの場合はbuilt-in serverがないのでconfig.ruを用意してrackupしたりするよね?
そのときに指定するクラスは自分のクラスになるわけで、つまりはそういうこと。

require './hello_world'
run HelloWorld

知ってる人からすりゃ当たり前な少し恥ずかしい話でした。