RSpecでテストHTTPサーバを立てて必要最小限のコンテンツで試験する方法


Rubyでブラウザの自動操作ライブラリの試験を書いていたときに、

こんな感じで必要最低限のHTMLコンテンツのページで試験がしたくなった。

Webmockを使う?

RSpecでHTTPテストサーバーを、みたいなのでぐぐると、なぜかよく出てくる。
https://github.com/bblimke/webmock

ただ、今回は最小コンテンツでHTTPサーバを立てたいのであって、モックしたいわけじゃないので、却下。

SinatraのDSLをRSpecで書けたら良さそうじゃない?

これは完全に自分の好みではあるが、

describe 'click button' do
  sinatra do
    get '/button.html' do
      <<~HTML
      <html>
        <head><title>button</title></head>
        <body>
          <button onclick='document.getElementById("mytext").innerHTML="Clicked!"'>
            Click here
          </button>
          <p id="mytext">Not Clicked Yet</p>
        </body>
      </html>
      HTML
    end
  end

  it 'can click button' do
    page.goto('/button.html')
    page.click('button')
    expect(page.Seval('p', 'el => el.textContent')).to eq("Clicked")
  end
end

こんな感じで、やりたいことをどストレートに書けたらいいなーと思った。

SinatraでHTTPサーバーを開始・終了するには?

Sinatraのサンプルコードだと、グローバルにHTTPサーバー定義をしているものが多いが、リファレンスやソースをよく読むと

app = Sinatra.new do
  get '/hello' do
    'Hello World'
  end
end

こんな感じでクラスベースでサーバーを定義することができるし、 run!quit! で開始・終了できることがわかる。

app.run! # 開始 (ブロッキングされるので別スレッド必須)

app.quit! # 終了

参考:https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb

sinatra do ... end を実装してみた

RSpec::Core::ExampleGroupに特異メソッドとして sinatra を定義すればいいので、大枠は以下のように。

module SinatraTestHttpDSL
  def sinatra(&block)
    ...
  end
end

RSpec::Core::ExampleGroup.extend(SinatraTestHttpDSL)

んで、sinatraメソッドの実装としては around フックを仕込んで example.runの前後で app.run!, app.quit! すればいい。

def sinatra(port: 4567, &block)
  # HTTPサーバーを定義
  app = Sinatra.new(&block)

  around do |example|
    # HTTPサーバーを起動
    Thread.new { app.run!(port: port) }

    # HTTPサーバーが疎通可能になるまで待つ。(雑w
    loop do
      Net::HTTP.get(URI("http://127.0.0.1:#{port}/"))
      break
    rescue Errno::ECONNREFUSED
      sleep 0.1
    end

    # テスト実行
    example.run

    # HTTPサーバーを終了
    app.quit!
  end
end

まとめ

てきとうに↓のように定義すれば

spec/spec_helper.rb
require 'bundler/setup'

# ここから ↓↓↓
module SinatraTestHttpDSL
  def sinatra(port: 4567, &block)
    require 'net/http'
    require 'sinatra/base'
    require 'timeout'

    # HTTPサーバーを定義
    app = Sinatra.new(&block)

    around do |example|
      # HTTPサーバーを起動
      Thread.new { app.run!(port: port) }

      # HTTPサーバーが疎通可能になるまで待つ
      Timeout.timeout(3) do
        loop do
          Net::HTTP.get(URI("http://127.0.0.1:#{port}/"))
          break
        rescue Errno::ECONNREFUSED
          sleep 0.1
        end
      end

      # テスト実行
      example.run

      # HTTPサーバーを終了
      app.quit!
    end
  end
end

RSpec::Core::ExampleGroup.extend(SinatraTestHttpDSL)
# ここまで ↑↑↑

RSpec.configure do |config|
  # Enable flags like --only-failures and --next-failure
  config.example_status_persistence_file_path = '.rspec_status'

 .... (後略) 

↓こんなかんじで必要最小限のコンテンツのHTTPサーバーを立てた状態でRSpec動かすことができる。

describe 'click button' do
  sinatra do
    get '/button.html' do
      <<~HTML
      <html>
        <head><title>button</title></head>
        <body>
          <button onclick='document.getElementById("mytext").innerHTML="Clicked!"'>
            Click here
          </button>
          <p id="mytext">Not Clicked Yet</p>
        </body>
      </html>
      HTML
    end
  end

  it 'can click button' do
    page.goto('/button.html')
    page.click('button')
    expect(page.Seval('p', 'el => el.textContent')).to eq("Clicked")
  end
end

実際にやったコード