Railsのシステムテスト（Minitest）で「期待どおりに404エラーが発生したこと」を検証する方法

Railsのシステムテスト（Minitest）で「期待どおりに404エラーが発生したこと」を検証する方法が意外と厄介だったので説明します。

### 対象バージョン

- Rails 6.1.5
- Capybara 3.36.0
- selenium-webdriver 4.1.0

### おことわり

この方法がベストなのか自信がないので、もっといい方法を知っている人がいたらコメント欄等教えてください🙏

## 404エラーの発生を検証したいユースケースの例

- ログイン済みユーザーしか表示させたくないページに、未ログインのユーザーがアクセスしてきた場合に404エラーを返すことをシステムテストで検証したい

302リダイレクトでトップページに遷移し、「ログインしてください」のメッセージを表示する、という挙動もありそうですが、ここではあえて404エラーを表示するものとします。

## ベースとなるテストコード

ここでは以下のようなテストコードを書いたものとします（上で書いたユースケースとは関係のない、単なる動作確認用のテストコードです）。

```ruby 
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エラーを検証するテストコードの書き方

今回はこんな方法でテストすることにしました。

```ruby
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`メソッドで元の値に戻すようにしました。

```ruby 
  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'` の部分はブラウザ上にエラーメッセージが表示されていることを検証しています。

![capybara-202203150908357562677473.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7465/25a54d2d-c035-16ec-e41d-9f844ee562a0.jpeg)


## 参考情報：RSpecの場合

RSpecであれば次のようなテストコードを書きます（rspec-rails 5.0.2で動作確認）。

### ドライバとしてrack_testを使っている場合

```ruby
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を使っている場合

```ruby 
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）の場合

結合テストの場合は比較的シンプルにテストが書けます。

```ruby 
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でもリクエストスペックで同じように書けると思います（未確認）。