はじめに
あるプロダクトではE2EテストをRspec + Capybara + Seleniumで実装しており、リリース前や本番作業後のシステムテストで利用しておりました。
しかし運用に支障をきたし始めたので改善する事にしました。
問題点
- テストが不安定
- なぜかMacだと問題ない部分でエラーになる確率が高く、わざわざWindows端末で実行していた(この為にWindowsをメンテンスしていくのは手間)
- 1度で全てのテストが通る事が少なく、恒常的に2~3回(失敗したテストのみ)テストを繰り返していた
- 時間がかかる
- テストそのものが遅く1連の実行に25分程度かかる
- 恒常的に2~3回実行していたのでテスト以外の余計な手間と時間がかかる
改善①ドライバをPlaywrightに変更する
Dockerファイル修正
- RUN apt-get update && \
- CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` && \
- wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ && \
- unzip ~/chromedriver_linux64.zip -d ~/ && \
- rm ~/chromedriver_linux64.zip && \
- chown root:root ~/chromedriver && \
- chmod 755 ~/chromedriver && \
- mv ~/chromedriver /usr/bin/chromedriver
+ WORKDIR /tmp
+ RUN npm init -y \
+ && npm install -D playwright \
+ && npx playwright install --with-deps \
+ && rm -rf /root/.npm /tmp/*
+ WORKDIR $APP_ROOT
Gemファイル修正
- gem "selenium-webdriver"
+ gem 'capybara-playwright-driver'
Rspec起動時のdriver設定を変更
RSpec.configure do |config|
config.before(:each, type: :system) do
+ playwright_timeout = (ENV['PLAYWRIGHT_TIMEOUT_MS'] || ENV['PLAYWRIGHT_TIMEOUT'] || 30000).to_i
+ driven_by(:playwright, screen_size: [1280, 1080], options: {
+ browser: :chromium,
+ headless: true,
+ timeout: playwright_timeout,
+ slow_mo: 50,
+ launch_options: { args: ['--no-sandbox', '--disable-dev-shm-usage'] }
+ })
- driven_by(:selenium_chrome_headless)
end
end
- Capybara.register_driver :selenium_chrome_headless do |app|
- browser_options = ::Selenium::WebDriver::Chrome::Options.new.tap do |opts|
- opts.args << "--headless"
- opts.args << "--disable-gpu"
- opts.args << "--no-sandbox"
- opts.args << "--disable-dev-shm-usage"
- opts.args << "window-size=1200,1080"
- end
- Capybara::Selenium::Driver.new(app, browser: :chrome, options: browser_options)
- end
+ capybara_wait = (ENV['CAPYBARA_DEFAULT_WAIT'] || 30).to_i
+ Capybara.default_max_wait_time = capybara_wait
Playwrightの自動待機機能を使うようにコードを変更
- 「要素が存在しない」の判定方法を変更
- expect(page.all("#hoge").present?).to eq false
+ expect(page).to have_no_css('#hoge', wait: 0)
-
sleep削除- Seleniumでは苦渋の選択としてsleepなどで明示的に待機しなければならない箇所が随所にありましたがこれを全削除
- javascriptでstyleのdisplay操作(inline/none)している箇所は、Playwrightの待機処理は非対応のようでした。ココだけは明示的にsleepする事にしました
ここまでの対応でテストの不安定さは無くなり、実行時間も20分程度には短縮されました🎉
改善②並列実行する
Gem(parallel_tests,parallel-rspec)の導入も検討しましたが、テスト結果をspecファイル単位で整然とまとめたいという「視認性」の要件を満たせず自作することにしました。
プロジェクト特有の背景として、テストケースとテストコードの整合性を担保するため、テスト結果をHTML出力した時にテストケースと見比べられる必要がありました。
カスタムコマンド作成
- ファイル単位にプロセスを起動してテストを実行します
- このプロダクトのシステムテストのspecファイルは5ファイル程度なので、同時実行プロセス数は何も考えていません
- あまりに冗長なので記載していませんが、実際にはテスト終了後に各結果HTMLファイルを1ファイルにマージする処理もいれています
#!/usr/bin/env ruby
require 'fileutils'
root = File.expand_path('..', __dir__)
Dir.chdir(root)
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
puts "Start: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
# 実行するspecファイルを収集する
arg = ARGV[0] || 'spec/system'
files = []
base_dir = nil
if File.file?(arg)
files = [arg]
base_dir = File.dirname(arg)
elsif Dir.exist?(arg)
base_dir = arg
files = Dir.glob(File.join(arg, '**', '*')).select { |f| File.file?(f) }
if files.empty?
puts "No files found in #{arg}"
exit 0
end
else
puts "File or directory not found: #{arg}"
exit 1
end
pids = []
tmp_dir = './tmp/para-rspec'
FileUtils.mkdir_p(tmp_dir)
# 前回の実行結果のHTMLを消す
File.delete('para-rspec_result.html') if File.exist?('para-rspec_result.html')
files.each do |file|
relative = file.sub(%r{^#{Regexp.escape(base_dir)}/?}, '')
outname = "rspec_resiult_#{relative.gsub('/', '_')}.html"
outpath = File.join(tmp_dir, outname)
File.delete(outpath) if File.exist?(outpath)
end
# ファイル単位にプロセスを起動してrspecを実行
files.each do |file|
relative = file.sub(%r{^#{Regexp.escape(base_dir)}/?}, '')
outname = "rspec_resiult_#{relative.gsub('/', '_')}.html"
outpath = File.join(tmp_dir, outname)
# HTMLで結果出力
cmd = ['bundle', 'exec', 'rspec', file, '--format', 'html', '--out', outpath]
#puts "Starting: #{cmd.join(' ')}"
env = ENV.to_hash
# Forward child stdout/stderr to the main process so outputs appear in real time
pid = Process.spawn(env, *cmd, chdir: root, out: STDOUT, err: STDERR)
pids << [pid, file, outpath]
end
success = true
pids.each do |pid, file, outname|
_, status = Process.wait2(pid)
result = status.exitstatus == 0 ? 'Success.' : 'Error!!'
puts "#{file} -> #{result}"
success &&= status.success?
end
テスト実行
bundle exec bin/para-rspec spec/system/
テスト結果
-
./tmp/para-rspecにspecファイルごとの結果が出力されます - 弊社ではおよそ5分で終わるようになりました(一番時間のかかるspecファイルの実行時間が5分)
おわりに
実際にテストが爆速になったのは良いのですが、一般的に存在する機能を自前で実装するのは保守性の観点からも頂けないので、なんとか落としどころを見つけてgemに乗り換えたいなぁ。。。
お知らせ
2026/5/28に博多でエンジニア向けのLT会開催します。
お食事&ドリンクでワイワイする会です。興味のある方は是非お越しください。
https://layered.connpass.com/event/378115/
また若手エンジニア募集しています。
自社サービスのフルスタックエンジニアや、要件定義・設計にも興味のある方は是非。
https://www.wantedly.com/projects/2297977