🍉

「Capybara、お前はただサーバーを起動してくれたらいいんだ!」って思ったときの対処法

2021/07/23に公開

https://zenn.dev/yusukeiwaki/articles/c88cb0c62b2ab3

で書いたように、RailsのSystem specなどで使うCapybaraは精度が良くない。

  • 「頼むから、Capybara DSLなんて使わないで、ネイティブのPuppeteerやPlaywrightでブラウザの自動操作をさせてくれ!」
  • 「Capybara、お前はただテスト用にRailsサーバーを起動してくれたらええんや!」

って思ったときに、どうすればいいか調べました。

CapybaraがHTTPサーバーを起動するように、ドライバ登録をする

「RailsアプリケーションをRackに乗せて、空いているポートを自動で見つけて、Puma起動する」というCapybaraがやっている仕事を自前で再実装するのは結構たいへんなので、ここだけ利用したい。

https://github.com/teamcapybara/capybara/blob/master/lib/capybara/server.rb

ただ、Capybara::Serverはどんな条件でも起動するわけではない。Capybaraドライバが needs_server? メソッドでtrueを返したときに限って起動するような実装になっている。

      @server = if config.run_server && @app && driver.needs_server?
        server_options = { port: config.server_port, host: config.server_host, reportable_errors: config.server_errors }
        server_options[:extra_middleware] = [Capybara::Server::AnimationDisabler] if config.disable_animation
        Capybara::Server.new(@app, **server_options).boot
      end

needs_server? はデフォルトではfalseが返り、Seleniumドライバなどブラウザを介して試験するようなドライバではtrueが返るように定義されている。

ただ、今回はべつに自動操作はCapybaraに任せるつもりはない。

  class NullDriver < Capybara::Driver::Base
    def needs_server?
      true
    end
  end

  Capybara.server = :puma, { Silent: false }
  Capybara.register_driver(:null) { NullDriver.new }
  Capybara.current_driver = :null

こんな感じで、needs_server? だけ定義したドライバをCapybaraに登録すればいいのだ。

Capybara::DSLを勝手にincludeしないようにする

rspec-railscapybara の2つのGemはかなり密結合で、Gemfileに gem 'capybara' って書くだけで、Capybara::DSLを勝手にロードしようとする。

  • rspec-railsは、Capybaraがあれば require 'capybara/rspec'require 'capybara/rails' などを勝手にincludeする
  • Capybaraは('capybara/rspec' の中で定義されているが)、 spec/features, spec/system 配下のファイルに対して、Capybara::DSLを勝手にincludeする

https://github.com/rspec/rspec-rails/blob/main/lib/rspec/rails/vendor/capybara.rb
https://github.com/teamcapybara/capybara/blob/master/lib/capybara/rspec.rb

勝手にCapybara::DSLをincludeさせないためには、spec/features, spec/system 以外のディレクトリ(たとえば、spec/integration など)にシステムテストのスクリプトを置くのが手っ取り早そうだ。

サンプル

例えば、 puppeteer-rubyをブラウザ操作部分に使うなら、こんな感じでaroundフックを使ってやればいい。

spec/integration/example_spec.rb
  around do |example|
    Capybara.current_driver = :null
    @server_base_url = Capybara.current_session.server.base_url

    Puppeteer.launch(channel: 'chrome', headless: false) do |browser|
      @puppeteer_page = browser.new_page
      example.run
    end

    Capybara.use_default_driver
  end
  let(:base_url) { @server_base_url }
  let(:page) { @puppeteer_page }

  it 'can browse' do
    page.goto("#{base_url}/test")
    page.wait_for_selector('input', visible: true)
    page.type_text('input', 'hoge')
    page.keyboard.press('Enter')
    expect(page.eval_on_selector('#content', 'el => el.textContent')).to include('hoge')
  end

Capybara.current_session を初めて参照したときにCapybaraはHTTPサーバーを起動するので、Puppeteer.launch(ブラウザ起動)よりも前に server_base_url = Capybara.current_session.server.base_url でしれっと current_sessionを参照しているのがポイント。

つづく...

続編かきました。「もうCapybaraなんて使わない!」って方法ですw
https://zenn.dev/yusukeiwaki/articles/449a860a750561

Discussion