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

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

![Slice.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74571/0ef33ac2-9a7d-426e-de59-990e711dd978.png)

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

## Webmockを使う?

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

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

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

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

```ruby
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](https://github.com/sinatra/sinatra)のサンプルコードだと、グローバルにHTTPサーバー定義をしているものが多いが、リファレンスやソースをよく読むと

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

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

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

app.quit! # 終了
```

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

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

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

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

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

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

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

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

    # HTTPサーバーが疎通可能になるまで待つ。（雑ｗ
    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
```

## まとめ

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

```ruby: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動かすことができる。

```ruby
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
```

## 実際にやったコード

https://github.com/YusukeIwaki/puppeteer-ruby/pull/10/files