Railsのシステムテスト(Minitest)で「期待どおりに404エラーが発生したこと」を検証する方法が意外と厄介だったので説明します。
対象バージョン
- Rails 6.1.5
- Capybara 3.36.0
- selenium-webdriver 4.1.0
おことわり
この方法がベストなのか自信がないので、もっといい方法を知っている人がいたらコメント欄等教えてください🙏
404エラーの発生を検証したいユースケースの例
- ログイン済みユーザーしか表示させたくないページに、未ログインのユーザーがアクセスしてきた場合に404エラーを返すことをシステムテストで検証したい
302リダイレクトでトップページに遷移し、「ログインしてください」のメッセージを表示する、という挙動もありそうですが、ここではあえて404エラーを表示するものとします。
ベースとなるテストコード
ここでは以下のようなテストコードを書いたものとします(上で書いたユースケースとは関係のない、単なる動作確認用のテストコードです)。
require "application_system_test_case"
class BlogsTest < ApplicationSystemTestCase
setup do
@blog = blogs(:one)
end
test "visiting a Blog" do
# 存在しないidを指定して、意図的に404エラーを発生させる
visit blog_url(@blog.id + 1)
end
end
このテストを実行すると、次のようなエラーメッセージが表示され、テストが失敗します。
2022-03-15 08:59:43 +0900 Rack app ("GET /blogs/980190963" - (127.0.0.1)): #<ActiveRecord::RecordNotFound: Couldn't find Blog with 'id'=980190963>
E
Error:
BlogsTest#test_visiting_a_Blog:
ActiveRecord::RecordNotFound: Couldn't find Blog with 'id'=980190963
app/controllers/blogs_controller.rb:63:in `set_blog'
rails test test/system/blogs_test.rb:16
Finished in 2.137874s, 0.4678 runs/s, 0.0000 assertions/s.
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
しかし、ここでは404エラーが発生するのはテストの失敗ではなく成功なので、「404エラーが期待どおり発生すればパス、発生しなければエラー」としたいです。
404エラーを検証するテストコードの書き方
今回はこんな方法でテストすることにしました。
class BlogsTest < ApplicationSystemTestCase
setup do
@blog = blogs(:one)
@raise_server_errors = Capybara.raise_server_errors
end
teardown do
Capybara.raise_server_errors = @raise_server_errors
end
test "visiting a Blog" do
Capybara.raise_server_errors = false
visit blog_url(@blog.id + 1)
assert_text 'ActiveRecord::RecordNotFound'
end
end
こうすると("ActiveRecord::RecordNotFound"のメッセージは表示されるものの)テストはパスします。
2022-03-15 09:04:50 +0900 Rack app ("GET /blogs/980190963" - (127.0.0.1)): #<ActiveRecord::RecordNotFound: Couldn't find Blog with 'id'=980190963>
.
Finished in 2.026935s, 0.4934 runs/s, 0.9867 assertions/s.
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
期待に反して404エラーが発生しなかった場合はテストが失敗します。
F
Failure:
BlogsTest#test_visiting_a_Blog [/Users/jnito/dev/sandbox/not-found-test-sandbox/test/system/blogs_test.rb:18]:
expected to find text "RecordNotFound" in "Title: MyString\nEdit | Back"
rails test test/system/blogs_test.rb:16
Finished in 4.355798s, 0.2296 runs/s, 0.2296 assertions/s.
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
変更点の解説
Capybara.raise_server_errors = false
を設定すると、404エラーが起きてもテストは失敗しなくなります。
ただし、単純にこの設定変更を加えるだけだと、全テストにこの変更が適用されてしまいます。そのため、setup
メソッドで元の値を保存し、teardown
メソッドで元の値に戻すようにしました。
setup do
@blog = blogs(:one)
# 元の値をインスタンス変数に保存する
@raise_server_errors = Capybara.raise_server_errors
end
teardown do
# 元の値に戻す
Capybara.raise_server_errors = @raise_server_errors
end
test "visiting a Blog" do
# このテストを実行するときだけ設定を変える
Capybara.raise_server_errors = false
visit blog_url(@blog.id + 1)
assert_text 'ActiveRecord::RecordNotFound'
end
assert_text 'ActiveRecord::RecordNotFound'
の部分はブラウザ上にエラーメッセージが表示されていることを検証しています。
参考情報:RSpecの場合
RSpecであれば次のようなテストコードを書きます(rspec-rails 5.0.2で動作確認)。
ドライバとしてrack_testを使っている場合
require 'rails_helper'
RSpec.describe "Blogs", type: :system do
before do
driven_by(:rack_test)
end
example 'Page not found' do
blog = Blog.create!(title: 'MyText')
# visitしたタイミングで例外が発生することを検証する
expect{ visit blog_url(blog.id + 1) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
ドライバとしてselenium_webdriverを使っている場合
require 'rails_helper'
RSpec.describe "Blogs", type: :system do
before do
driven_by(:selenium_chrome_headless)
end
describe '404 error' do
# aroundフックを使って、Minitestでいうところのsetupとteardownと
# 設定値の変更を一気に行う
around do |example|
original = Capybara.raise_server_errors
Capybara.raise_server_errors = false
example.run
Capybara.raise_server_errors = original
end
example 'Page not found' do
blog = Blog.create!(title: 'MyText')
visit blog_path(blog.id + 1)
# ブラウザ上にエラーメッセージが表示されていることを検証する
expect(page).to have_text 'ActiveRecord::RecordNotFound'
end
end
end
結合テスト(IntegrationTest)の場合
結合テストの場合は比較的シンプルにテストが書けます。
require 'test_helper'
class BLogsTest < ActionDispatch::IntegrationTest
setup do
@blog = blogs(:one)
end
test "visiting a Blog" do
assert_raise(ActiveRecord::RecordNotFound) { get blog_url(@blog.id + 1) }
end
end
RSpecでもリクエストスペックで同じように書けると思います(未確認)。