🍉

CapybaraなしでRailsアプリのE2Eテスト(feature spec)を実行する

2021/09/01に公開

Capybaraはわりと万能だが、「別にそこまでやってくれなくていい」というところが多い。

https://zenn.dev/yusukeiwaki/articles/17a166d16b12fa

Capybaraを最小限使ってラクする方法は↑の記事で書いたが、完全にCapybaraなしでRailsアプリをブラウザと対向させて試験する方法も調べたのでメモ。

Capybaraなしとはどういう状態か

基本的には、以下の3点以外は変わらない。

  • テストサーバーは起動しない
  • Capybara::DSLがincludeされない
    • visitなどは未実装の例外(NotImplementedError)ではなく、未定義(NameErrorあるいはNoMethodError)になる。
  • system specはSystemTestCaseがCapybaraに強く依存しているので、使えない

たとえばrspec-railsは以下のようなセットアップをしてくれるが、これはCapybaraの有無に関係なく行われる。

  • spec/features配下を type: :feature spec/system 配下を type: :system とメタデータ付ける
  • feature specではdescribe, before, it の代わりに feature, background, scenario を使う

テスト開始時にサーバーを起動する(実験)

ようするにサーバーさえ起動すればいいので、Capybaraの以下の部分でやっていることをマネする。

Capybaraは親切なのでいろいろやっているが、なるべくシンプルにいきたいので以下のように妥協する。

  • Webrickは使わないので、Puma起動ロジックのみにする
  • ポートは3000番に固定する
  • ホストも127.0.0.1に固定する

そうすれば、なんと以下のような設定だけでサーバーが起動してE2Eテストできる。

spec/support/integration_test_helper.rb
RSpec.configure do |config|
  config.before(:suite) do
    # Railsサーバー起動
    require 'rack/builder'
    testapp = Rack::Builder.app(Rails.application) do
      map '/__ping' do # サーバー起動したかどうかの確認用エンドポイント
        run ->(env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
      end
    end

    require 'rack/handler/puma'
    server_thread = Thread.new do
      Rack::Handler::Puma.run(testapp,
        Port: 3000,
        Threads: '0:4',
        workers: 0,
        daemonize: false,
      )
    end

    # Railsサーバーが起動完了してHTTPアクセスできるようになるまで待つ
    require 'net/http'
    require 'timeout'
    Timeout.timeout(3) do
      loop do
        puts Net::HTTP.get(URI("http://127.0.0.1:3000/__ping"))
        break
      rescue Errno::EADDRNOTAVAIL
        sleep 1
      rescue Errno::ECONNREFUSED
        sleep 0.1
      end
    end
  end

  # 自動操作をpuppeteer-rubyで行う
  config.around(type: :feature) do |example|
    Puppeteer.launch(channel: :chrome, headless: false) do |browser|
      @server_base_url = 'http://127.0.0.1:3000'
      @puppeteer_page = browser.new_page
      example.run
    end
  end
end

specはこの記事のときと同様、以下のようなもの。
冒頭で書いたように、Railsが提供するシステムテスト(system spec)は使えないので、rspec-railsが提供するfeature specの方を使う。

spec/features/example_spec.rb
require 'rails_helper'

describe 'example' do
  let(:base_url) { @server_base_url }
  let(:page) { @puppeteer_page }

  let(:user) { FactoryBot.create(:user) }

  it 'can browse' do
    page.goto("#{base_url}/tests/#{user.id}")
    page.wait_for_selector('input', visible: true)
    page.type_text('input', 'hoge')
    page.keyboard.press('Enter')

    text = page.eval_on_selector('#content', 'el => el.textContent')
    expect(text).to include('hoge')
    expect(text).to include(user.name)
  end
end

実行すると、Capybaraのときと遜色なく実行できるし、Ctrl+Cでテスト中断も問題なく(サーバープロセスが変に残ったりせず)できた。

テスト開始時にサーバーを起動(ちょっとリファクタリング)

いくらなんでも前章のintegration_test_helper内容を「全部こぴぺして自分のアプリケーションに導入するぞ!」というのは気がひける。(書いてる私自身もやりたくないw)

そんなわけで、サクッと使えるようにGemにしました。

https://rubygems.org/gems/rack-test_server

Rack::Handler::Puma を直接利用していた部分を、 Rack::Server#start という rackup コマンドの実装でも使われている汎用的なものに変更したほかは同じ処理をしています。

spec/support/integration_test_helper.rb
RSpec.configure do |config|
  config.before(:suite) do
    # Railsサーバー起動(rackup相当の動作)
    server = Rack::TestServer.new(
      # Rack::Serverのオプション
      # https://github.com/rack/rack/blob/2.2.3/lib/rack/server.rb#L173
      app: Rails.application,
      server: :puma,
      Port: 3000,
      daemonize: false,
      
      # Rack::Handler::Pumaのオプション
      # https://github.com/puma/puma/blob/v5.4.0/lib/rack/handler/puma.rb#L84
      Threads: '0:4',
      workers: 0,
    )
    
    server.start_async
    server.wait_for_ready
  end

だいぶ再利用性が向上してますね。(自画自賛w)

まとめ

「Capybaraじゃなくて、素でSelenium使いたい」 「Capybaraじゃなくて、素でPlaywright使いたい」 なんてことは意外と簡単にできる。

Discussion