CapybaraなしでRailsアプリのE2Eテスト(feature spec)を実行する
Capybaraはわりと万能だが、「別にそこまでやってくれなくていい」というところが多い。
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の以下の部分でやっていることをマネする。
- https://github.com/teamcapybara/capybara/blob/master/lib/capybara/server.rb
- https://github.com/teamcapybara/capybara/blob/master/lib/capybara/registrations/servers.rb
Capybaraは親切なのでいろいろやっているが、なるべくシンプルにいきたいので以下のように妥協する。
- Webrickは使わないので、Puma起動ロジックのみにする
- ポートは3000番に固定する
- ホストも127.0.0.1に固定する
そうすれば、なんと以下のような設定だけでサーバーが起動してE2Eテストできる。
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の方を使う。
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にしました。
Rack::Handler::Puma
を直接利用していた部分を、 Rack::Server#start
という rackup
コマンドの実装でも使われている汎用的なものに変更したほかは同じ処理をしています。
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