QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

RSpec+SeleniumでE2Eテストを書く最小構成(Ruby)

Selenium・自動化

なぜ手動でのポチポチ確認ではなくE2Eなのか

小規模な業務用スクリプトやツールを作っていると、「ログイン→フォーム入力→送信→結果画面の確認」といった一連の操作を、リリースのたびに手動でブラウザを開いて確認する、というやり方をしばらく続けてしまいがちです。最初のうちは数分で終わるので気になりませんが、機能が増えるにつれて確認項目も増え、気づけば毎回15分、20分とかかるようになり、しかも「今日は忙しいから軽く流し見しただけ」という日が混ざるようになります。

この手動確認は、うっかり手順を飛ばしたり、同じ画面を毎回同じように見ているうちに変化を見落としたりする、という形で確実に劣化します。E2E(End-to-End)テストとしてSeleniumでブラウザ操作を自動化しておけば、確認の手順そのものがコードとして固定され、毎回同じ厳密さで実行できます。しかも一度書いてしまえば、実行コストは「コマンド一つ叩く」程度まで下がります。

ここでは、Rubyで最小限の構成からRSpec+SeleniumのE2Eテストを組み立てる手順を、実際に動くコードとともに紹介します。

Gemfileは最小構成でよい

E2Eテストを始めるために必要なgemは、基本的にrspecselenium-webdriverの2つだけです。

source "https://rubygems.org"

gem "rspec", "~> 3.13"
gem "selenium-webdriver", "~> 4.23"

Selenium 4系はブラウザに対応するドライバ(chromedriverなど)を自動で解決してくれる「Selenium Manager」を内蔵しているため、以前のようにwebdriversgemでドライバのバージョン管理をする必要がなくなっています。まずはこの2行から始めて、必要になった時点でrspec-retryやレポート出力用のgemを足していく、という進め方で十分です。

spec_helperでdriverの起動・終了をbefore/afterにまとめる

spec_helper.rbに、各テストの前後でブラウザを起動・終了する処理をまとめておきます。テストごとに新しいブラウザインスタンスを立ち上げることで、Cookieやセッションが前のテストの影響を受けないようにします。

require "selenium-webdriver"

RSpec.configure do |config|
  config.before(:each) do
    options = Selenium::WebDriver::Chrome::Options.new
    options.add_argument("--headless=new") if ENV["HEADLESS"] == "true"
    options.add_argument("--window-size=1280,800")

    @driver = Selenium::WebDriver.for(:chrome, options: options)
  end

  config.after(:each) do |example|
    take_failure_screenshot(example) if example.exception

    @driver&.quit
  end
end

options.add_argument("--headless=new")を環境変数HEADLESSで切り替えられるようにしているのは、手元で書いているときはブラウザの動きを目で追いたい一方、CI上では画面のないheadlessモードで動かしたい、という両方の要求を1つの設定で満たすためです。

サンプルのログインフォームをテストする

対象は架空のサービス(https://example.com)にある、汎用的なログインフォームだとします。メールアドレスとパスワードを入力して送信すると、ダッシュボード画面に遷移する、というよくある構成です。

require "spec_helper"

RSpec.describe "サンプルのログインフォーム" do
  it "正しい認証情報でログインするとダッシュボードが表示される" do
    @driver.navigate.to "https://example.com/login"

    @driver.find_element(:id, "email").send_keys("user@example.com")
    @driver.find_element(:id, "password").send_keys("password123")
    @driver.find_element(:css, "button[type='submit']").click

    dashboard_title = Selenium::WebDriver::Wait.new(timeout: 5).until do
      element = @driver.find_element(:css, "h1.dashboard-title")
      element if element.displayed?
    end

    expect(dashboard_title.text).to eq("ダッシュボード")
  end
end

ポイントは、送信直後に要素を探しにいくのではなく、Selenium::WebDriver::Waitで「成功後にしか現れない要素(ここではh1.dashboard-title)」が表示されるまで待ってから検証している点です。ログイン処理はサーバー側の応答を待つ非同期処理なので、待機なしで検証すると、環境やネットワークの状態次第でテストがまれに失敗する「フレーキーテスト」になりやすくなります。

失敗したときだけスクリーンショットを残す

E2Eテストがなぜ失敗したのかは、エラーメッセージだけでは分からないことが多く、失敗時点の画面を見られると原因調査が一気に早くなります。そこで、example.exceptionが存在する場合、つまりそのテストが失敗した場合にだけスクリーンショットを保存するフックをspec_helper.rbに足します。

def take_failure_screenshot(example)
  return unless @driver

  Dir.mkdir("screenshots") unless Dir.exist?("screenshots")

  safe_name = example.full_description.gsub(/[^0-9A-Za-z]+/, "_")
  path = "screenshots/#{safe_name}_#{Time.now.to_i}.png"

  @driver.save_screenshot(path)
  warn "スクリーンショットを保存しました: #{path}"
rescue => e
  warn "スクリーンショットの保存に失敗しました: #{e.message}"
end

テストの説明文(full_description)をファイル名に含めているのは、複数のテストが同時に失敗したときに、どの失敗がどのスクリーンショットなのか一目で分かるようにするためです。記号や日本語がそのままだとファイルシステムによっては扱いにくいため、英数字以外を_に置き換えています。

CI上でも同じテストをそのまま動かす

ここまでの構成は、ローカルでもCIでも同じコードで動きます。GitHub Actionsであれば、Chromeとchromedriverが用意された環境でHEADLESS=trueを渡して実行するだけです。

name: e2e

on: [push]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true
      - run: bundle exec rspec
        env:
          HEADLESS: "true"

失敗時のスクリーンショットは、CI上ではactions/upload-artifactなどでアーティファクトとして回収しておくと、ローカルで再現しない失敗の調査にも役立ちます。ローカルとCIの違いはHEADLESS環境変数だけに閉じ込めてあるので、「ローカルでは通るのにCIでは落ちる」という設定差分そのものを減らせるのも、この構成の利点です。

まとめ

  • 手動でのブラウザ確認は手順の抜け漏れや見落としが避けられないため、繰り返し行う確認はE2Eテストとしてコード化しておく
  • Gemfileはrspecselenium-webdriverの2行から始められ、Selenium 4系はドライバの解決も自動でやってくれる
  • spec_helper.rbbefore(:each)/after(:each)でブラウザの起動・終了を管理し、テストごとに独立した状態で実行する
  • 送信直後ではなく「成功後にだけ現れる要素」が表示されるまでWaitで待ってから検証し、フレーキーテストを避ける
  • 失敗時だけスクリーンショットを保存するフックを用意しておくと、原因調査のスピードが大きく変わる
  • ローカルとCIの違いをHEADLESS環境変数1つに閉じ込めておけば、同じテストコードをそのまま両方の環境で動かせる

コメント

タイトルとURLをコピーしました