🔥

RSpec の Feature Spec を System Spec に置き換えた

2024/12/04に公開

今回は、playwright-ruby-client を導入した際の手順を残したいと思います。

弊社ではこれまで E2E テストを RSpec + Capybara + Selenium で行なっていました。しかし、この構成では Flaky の発生頻度が高く実行時間も非常に長かったため、CI 高速化の取り組みの一つとして RSpec + Capybara + playwright-ruby-client + Playwright にリプレースを行いました。

Playwright には以下の利点があり非常に使いやすいです。

  • クロスブラウザテストのサポート: PlaywrightはChromium、Firefox、WebKitなど複数のブラウザをサポートしており、単一のAPIでクロスブラウザテストを実行できます。
  • 自動化機能の充実: Playwrightは、ユーザーの動きに近い形でテストケースを自動生成する機能を持っています。
  • 高速なテスト実行: Playwrightはブラウザを並列で実行できるため、テストの待ち時間が短縮されます。また、テスト項目ごとにブラウザが初期化されるため、一貫した環境でのテスト結果が保証されます。
  • 詳細なログとデバッグ機能: テストが失敗した場合には、動画やスクリーンショット、DOM状態など詳細なログが生成されるため、デバッグが容易です。
  • CI/CDパイプラインへの統合: PlaywrightはCIツールとの統合が容易であり、GitHub ActionsやJenkinsなどで簡単に使用できます。
  • 豊富な機能セット: スクリーンショットやビデオキャプチャ、ネットワークトラフィックのインターセプトなど、多様な機能を提供しており、さまざまなシナリオでのテストが可能です。

playwright-ruby-client の導入手順

まず Capybara が Playwright の API を使用できる様にするための Driver を導入する必要があります。
capybara-plawright-driver という Gem を playwright-ruby-client の作者が公開しているのですが、以下の理由から利用せず Driver を自作しました。

  • Capybara の API を介して Playwright の API を使用すると仕様の関係上、Playwright の API を直接使用したのと比較して不正確さが高い(Flaky になりやすい)
  • Playwright の便利な API が一部使用できない
  • selenium-webdriver を単純に capybara-plawright-driver 置き換えて使用できるわけではなく、多くのテストコードに修正が発生する

Playwright Driver

自作した Driver は以下のようになります。一部の selenium-webdriver でつかえる Util 関数を使用できるようにするために #page, #visit 等を Drive に定義し @playwright_page.define_singleton_method を使用して playwright-ruby-client に関数を追加し、追加した関数内で Drive に定義した関数を実行しています。
(自作した Driver は公式に載っている手順に従って作成しています。)

spec/rails_helper.rb
# CI 上で実行されているかどうか
on_ci = ENV.has_key?('CI')
# Docker 上で実行されているかどうか
on_docker = File.exist?('/.dockerenv')

class PlaywrightNullDriver < Capybara::Driver::Base
  def needs_server? = true
  def wait? = true

  def page=(val)
    @page = val
  end

  def page
    @page
  end

  def visit(path)
    path
  end

  def current_url
    page&.url
  end

  def html
    page&.content
  end
end

Capybara.register_driver(:playwright) { PlaywrightNullDriver.new }

RSpec.configure do |config|
  # ...省略
  config.around(:each, type: :system) do |example|
    driven_by :rack_test

    Capybara.current_driver = :playwright

    # Rails server is launched here, at the first time of accessing Capybara.current_session.server
    base_url = Capybara.current_session.server.base_url
    # When running in CI or Docker, we run the browser in headless mode
    headless = on_ci || on_docker

    executor = if on_ci || on_docker
      playwright_host = ENV.fetch('PLAYWRIGHT_HOST', 'localhost')            
      playwright_port = ENV.fetch('PLAYWRIGHT_PORT', '8080')
      playwright_server = "ws://#{playwright_host}:#{playwright_port}/ws?browser=chromium"
      Playwright.connect_to_playwright_server(playwright_server)
    else
      playwright_cli_executable_path = Rails.root.join('./node_modules/.bin/playwright')
      Playwright.create(playwright_cli_executable_path:)
    end

    executor.playwright.chromium.launch(headless:, slowMo: 100) do |browser|
      browser.new_context(baseURL: base_url, ignoreHTTPSErrors: true) do |context|
        @playwright_browser = browser
        @playwright_page = context.new_page

        Capybara.current_session.driver.page = @playwright_page

        context.enable_debug_console! unless headless

        # Override the save_screenshot method that is used by capybara
        @playwright_page.define_singleton_method(:save_screenshot) do |path|
          dir = File.dirname(path)
          FileUtils.mkdir_p(dir)

          context.pages.last.screenshot(path: path, fullPage: true)
        end

        # goto メソッドをオーバーライドし、Capybara の visit メソッドと同じように動作するようにする
        @playwright_page.define_singleton_method(:goto) do |path, timeout: nil, waitUntil: nil, referer: nil|
          url = Capybara.current_session.visit(path)
          main_frame.goto(url, timeout: timeout, waitUntil: waitUntil, referer: referer)
        end

        def browser
          @playwright_browser
        end

        def page
          @playwright_page
        end

        example.run
      end
    end

    executor.stop
  end
end

mesoddonotikann

selenim-drive のメソッドを playwright-ruby-client のメソッドへの置き換え作業は、ほぼほぼ正規表現を使用した置換で対応することができます。
playwright-ruby-driver の API の理解につながるので、この部分はぜひ selenium-webdrive のメソッドと playwright-ruby-driver のメソッドを比較して対応してみてください。

さらなる高速化

playwright の導入に加えて以下を使用することでさらにテストの高速化を目指すことができます。

  • System Spec の transactional_fixture 機能を ON に、データベースのトランザクションを使用したデータの初期化を行える様にする。
  • test-proflet_it_be を使用し、データの作成回数を必要最低限にする。

導入した感想

Capybara + Selenium と比較して Capybara + Playwright は実際に以下の点で優れていました。

  • Flaky なテストが大きく減らせた
  • 実行時間もコンスタントにへる
  • Selenium Driver には要素の表示を待つ関数がないため wait を使用して一定の時間必ず待つというコードがあったが、これを削除できるので実行時間の削減にダイレクトにつながる
  • Playwright 自体に GUI のデバッグツールが組み込まれているため、テストの検証・作成・修正のコストが大きく減らせる

課題

現状、ブラウザに表示されている日本語を探させようとすると稀にエラーが発生してしまう問題が発生しており解決できていません。
エラーログを見るかぎる文字コードが原因の様に見えるのですが、playwright-client-ruby のコードを読んでも、デバッグして文字コードを確認してみても UTF-8 となっており問題ないため原因がわかっていません。
このエラーが発生すると JUnit 形式のテスト結果ファイル出力にも失敗するようになってしますうのでどうにかして解決したいです。

Discussion