11 人が閲覧(直近 30 日) テスト

request specとは?Rails 8.1での書き方とログイン・422・JSON検証

request specとは?Rails 8.1での書き方とログイン・422・JSON検証

request spec(リクエストスペック)は、HTTPリクエストを1本送り、返ってきたHTTPレスポンスを検証するRSpecのテストです。コントローラのメソッドを直接呼ばず、ルーティング、ミドルウェア、コントローラ、モデル、レスポンス生成までを実際に通します。この記事のコードはすべて Rails 8.1.4 と rspec-rails 8.0.4 の検証アプリで実行し、通ることを確かめたものです。2022年以前の解説が扱っていない、Rails 8 の認証ジェネレータを使ったアプリでのログイン方法と、Rack 3.1 で変わった 422 の書き方も取り上げます。

まとめ

  • request spec は「HTTPリクエストを送ってレスポンスを検証する」テストです。RSpec 3.5 以降、Rails と RSpec の両チームがコントローラの直接テストを勧めておらず、controller spec の代わりとして使われています。
  • 置き場所は spec/requests/ です。rspec-rails 8.0.4 が生成する rails_helper.rb ではファイルの場所から種類を推測する設定が無効なので、type: :request を必ず書きます。
  • Rails 8 の認証ジェネレータを RSpec 環境で実行すると、Minitest 用のログインヘルパーは作られません。scaffold が出力する request spec はそのままだと 302 で落ちるので、ログイン用のヘルパーを自分で置きます。
  • 422 は :unprocessable_content で書きます。Rack 3.1(2024年6月)で旧名 :unprocessable_entity は非推奨になり、テストは通るものの実行のたびに警告が出ます。
  • JSONの中身は response.parsed_body で検証します。be_json というマッチャは RSpec に存在しません。
  • ログインを含む同じ20例を3回ずつ実行した中央値は、request spec 1.03秒、ヘッドレスChromeの system spec 24.80秒でした。HTTPの入出力で表せる仕様は request spec で書きます。

request specの守備範囲とcontroller spec非推奨の経緯

rspec.info の request spec の解説は、request spec を「Railsの結合テストへの薄いラッパーで、ルーティングを含むフルスタックをスタブなしで動かすもの」と説明しています。ルーティングを含む点が controller spec との違いです。controller spec でも before_action による認証は検証できますが、routes.rb の書き間違いやミドルウェアの挙動は検証範囲の外です。request spec はURLとHTTPメソッドから始まるので、そこまで含めて確認できます。

controller spec との関係は決着済みです。rspec-rails 8.0.4 の README は request spec を controller spec の高水準な代替と位置づけ、「RSpec 3.5 の時点で、Rails と RSpec の両チームがコントローラを直接テストすることを勧めなくなった(as of RSpec 3.5, both the Rails and RSpec teams discourage directly testing controllers)」と書いています。新しく書くテストで controller spec を選ぶ理由はありません。既存の controller spec は、そのコントローラを触るときに request spec へ移すのが無理のない進め方です。

同じ README は request spec を「機械のクライアントから見たアプリのテスト」とし、UIやJavaScriptは調べないと明記しています。rspec.info も Capybara は request spec では使えないと書いています。画面のボタン操作やJavaScriptの結果を確かめたいなら system spec の担当です。その構築手順はRSpecでCapybaraとPlaywrightを使う設定手順|capybara-playwright-driverとRailsの注意点で扱っています。

rspec-railsの導入とspec/requestsのファイル構成

rspec-rails 8.0.4とRSpec本体の版の関係

RSpec はテストフレームワーク本体(rspec-core・rspec-expectations・rspec-mocks)で、rspec-rails はそれを Rails に組み込むgemです。版番号は別々に進んでいて、2026年11月時点の最新は rspec-core 3.13.6、rspec-rails 8.0.4(2026年3月10日)です。「RSpec 8」という版は無く、8 は rspec-rails の版番号です。

rspec-rails 8.0.4 の gemspec は actionpack・railties の 7.2 以上を要求します。README の対応表は「rspec-rails 8.x は Rails 8.0 か 7.2 向け」とだけ書いていますが、Rails 8.1.4 でもこの記事のコードは全件通りました。Rails 7.1 以前なら rspec-rails 7.x 以前を選びます。

RSpec 4.0.0.beta1 は2026年2月18日に公開されたプレリリース版です。rspec-rails の変更履歴によると、8.0.4 は「4.0.0.beta1 を許容するために依存の上限を緩めたリリース」です。Bundler はプレリリース版を明示しない限り選ばないので、bundle update で勝手に4系へ上がることはありません。RSpec.describe と expect を使う書き方は4系への移行にも適しています。ただし、4系には設定項目の削除や共有コンテキストの挙動変更もあるため、この2点だけで互換性は保証できません。

rspec:installとtype: :requestの指定

Gemfile の development / test グループに rspec-rails を追加します。テストデータの生成には factory_bot_rails を併用します。

# Gemfile
group :development, :test do
  gem "rspec-rails", "~> 8.0.4"
  gem "factory_bot_rails", "~> 6.5"
end

bundle install のあとにインストールジェネレータを実行すると、設定ファイルと spec ディレクトリが作られます。

$ bin/rails generate rspec:install
      create  .rspec
      create  spec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

生成された spec/rails_helper.rb の RSpec.configure に、FactoryBot の create や create_list を spec 内でそのまま呼ぶための1行を足します。

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include FactoryBot::Syntax::Methods
end

注意したいのは、同じファイルの config.infer_spec_type_from_file_location! がコメントアウトされた状態で生成される点です。この設定が無効だと、spec/requests/ に置いただけでは request spec として扱われません。type: :request を書き忘れたファイルを実行すると、次のように get が見つからずに落ちます。

NoMethodError:
  undefined method 'get' for #<RSpec::ExampleGroups::Type:0x0000000111a4a3a0>

RSpec.describe "Articles", type: :request do と毎回書くか、rails_helper.rb のコメントを外して推測を有効にするかのどちらかです。後から参加した人が設定を知らなくても読めるので、明示する方を勧めます。

rspec:requestジェネレータとscaffold雛形の使い分け

単独のファイルは専用のジェネレータで作れます。

$ bin/rails generate rspec:request Comments
      create  spec/requests/comments_spec.rb

中身は RSpec.describe "Comments", type: :request do の枠と、get comments_index_path を呼ぶ例1本だけです。そのルートが無ければ NameError で落ちるので、書き換える前提の雛形と考えてください。

bin/rails generate scaffold を実行した場合は、rspec-rails が index・show・new・edit・create・update・destroy の request spec をまとめて出力します。valid_attributes と invalid_attributes が skip で空けてあり、そこを埋めると動く作りです。Article の scaffold で生成すると13例のうち12例が pending、GET /new の1例だけが実行されました。書き方の型を覚えるには、この出力を読むのが早道です。ジェネレータ全般の生成物と取り消し方はRailsジェネレータ(rails generate)の使い方|生成ファイル一覧と取り消し・カスタム生成【Rails 8.1.4】にまとめています。

JSON APIのHTTPメソッド別の書き方

この章のコードは、title に presence バリデーションを持つ Article モデルと、ActionController::API を継承した Api::ArticlesController(/api/articles)に対して実行し、0 failures を確認したものです。

GETリクエストでの一覧取得とステータス確認

リクエストは get "/api/articles" のようにパスの文字列で書けます。ステータスは have_http_status、本文は response.parsed_body で検証します。

require "rails_helper"

RSpec.describe "Api::Articles", type: :request do
  describe "GET /api/articles" do
    it "200 と登録済みの記事一覧を返す" do
      create_list(:article, 3)
      get "/api/articles"
      expect(response).to have_http_status(:ok)
      expect(response.parsed_body.size).to eq 3
    end
  end
end

have_http_status には :ok のようなシンボル、200 のような数値、:success のような分類を渡せます。:success は「2xxのどれか」という緩い検証になるので、ステータスコードを仕様として固定したいときはシンボルか数値を使います。

POSTリクエストでのレコード増加とcreated判定

作成系では、レコードが増えたかと、返ったステータスと本文が仕様どおりかの2点を確かめます。前者は change マッチャをブロック付きで使います。

describe "POST /api/articles" do
  it "記事が1件増え、201 を返す" do
    expect {
      post "/api/articles", params: { article: { title: "新規記事", body: "本文" } }, as: :json
    }.to change(Article, :count).by(1)
    expect(response).to have_http_status(:created)
    expect(response.parsed_body["title"]).to eq "新規記事"
  end

  it "title が空なら 422 を返し、記事は増えない" do
    expect {
      post "/api/articles", params: { article: { title: "", body: "本文" } }, as: :json
    }.not_to change(Article, :count)
    expect(response).to have_http_status(:unprocessable_content)
    expect(response.parsed_body["title"]).to include("can't be blank")
  end
end

増えないことは not_to change で書きます。by(0) でも同じ結果ですが、not_to の方が意図を読み取りやすくなります。

PATCHリクエストでの更新反映の確認

更新系で漏れやすいのが reload です。spec 内の article はリクエスト前に読み込んだオブジェクトなので、リクエストで更新されても古い値のままです。article.reload.title とデータベースから読み直して初めて、更新が保存されたかを確認できます。

describe "PATCH /api/articles/:id" do
  it "title が書き換わる" do
    article = create(:article, title: "更新前")
    patch "/api/articles/#{article.id}", params: { article: { title: "更新後" } }, as: :json
    expect(response).to have_http_status(:ok)
    expect(article.reload.title).to eq "更新後"
  end
end

DELETEリクエストでのレコード減少と404の確認

削除は change(…).by(-1) で件数の減少を確かめます。本文を返さない destroy は head :no_content で 204 を返すので、:ok を期待すると落ちます。

describe "DELETE /api/articles/:id" do
  it "記事が1件減り、204 を返す" do
    article = create(:article)
    expect {
      delete "/api/articles/#{article.id}"
    }.to change(Article, :count).by(-1)
    expect(response).to have_http_status(:no_content)
  end
end

it "存在しない記事は 404 を返す" do
  get "/api/articles/999999"
  expect(response).to have_http_status(:not_found)
end

find で ActiveRecord::RecordNotFound が起きても、今回のテスト環境では config.action_dispatch.show_exceptions = :rescuable のため、404 のレスポンスになりました。この設定が :none なら例外が送出されます。存在しないIDの扱いはステータスで検証します。

headersとas: :jsonによるリクエスト形式の指定

トークン認証のAPIなど、ヘッダーが仕様に含まれるときは headers: で渡します。送ったヘッダーは request.headers で、返ったレスポンスの種類は response.media_type で確かめられます。

it "headers: で Authorization などを付けて送れる" do
  get "/api/articles", headers: { "Authorization" => "Bearer test-token", "Accept" => "application/json" }
  expect(request.headers["Authorization"]).to eq "Bearer test-token"
  expect(response.media_type).to eq "application/json"
end

it "as: :json は Content-Type を application/json にする" do
  post "/api/articles", params: { article: { title: "x" } }, as: :json
  expect(request.media_type).to eq "application/json"
end

as: :json はパラメータをJSONにして送り、Content-Type を application/json にするオプションです。省略するとフォーム形式で送られます。JSON APIの spec では付けておくと、どの形式を検証しているのかが読み手に伝わります。JWTの発行と検証を含むAPIの組み立てはRails APIとは?作成手順・JWT認証・React連携・Docker環境を実装解説【Rails 8.1】で扱っています。

HTML画面のrequest spec:redirect_to・follow_redirect!・flash

request spec はJSON APIのためだけのものではありません。scaffold が作る通常の画面でも、リダイレクト先、flash、描画された本文を検証できます。以下の例は type: :request のグループ内に置き、認証を導入したアプリでは後述の sign_in_as ヘルパーで各例の前にログインします。

it "作成後は詳細へリダイレクトし、flash が表示される" do
  post articles_path, params: { article: { title: "新規", body: "本文" } }
  expect(response).to redirect_to(article_path(Article.last))
  expect(flash[:notice]).to eq "Article was successfully created."
  follow_redirect!
  expect(response).to have_http_status(:ok)
  expect(response.body).to include("Article was successfully created.")
end

it "title が空なら 422 で new を描き直す" do
  post articles_path, params: { article: { title: "" } }
  expect(response).to have_http_status(:unprocessable_content)
  expect(response.body).to include("can&#39;t be blank")
end

it "更新は 303 See Other で詳細へ戻す" do
  article = create(:article)
  patch article_path(article), params: { article: { title: "更新後" } }
  expect(response).to have_http_status(:see_other)
  expect(response).to redirect_to(article_path(article))
end

押さえどころは3つあります。この例の follow_redirect! はリダイレクト先へGETを送り、その後の response は詳細画面のものに入れ替わります。307・308の場合は、元のHTTPメソッドを引き継ぎます。HTMLではアポストロフィが &#39; にエスケープされるため、include に渡す文字列もエスケープ後の形にします。Rails 8.1 の scaffold は update と destroy で status: :see_other(303)を返すので、302 を期待して書くと落ちます。

ログインが必要なエンドポイントのrequest spec

実際のアプリでは、ほとんどの画面がログインを前提にしています。認証方式によってテストの準備は異なります。特に詰まりやすいのが、Rails 8 から標準になった認証ジェネレータ(bin/rails generate authentication)を RSpec と組み合わせる場合です。

Rails 8認証ジェネレータとRSpecのヘルパー未生成・root未定義・メール重複

Minitest 環境で認証ジェネレータを実行すると、Rails は test/test_helpers/session_test_helper.rb を作り、sign_in_as(user) でログインできるようにします。ところが rspec-rails を入れた環境ではテスト生成が rspec-rails 側に切り替わり、rspec-rails の認証ジェネレータが作るのは User の model spec とテストデータだけです。ログイン用のヘルパーはどこにもできません。

この状態で scaffold を追加し、生成された request spec の valid_attributes を埋めて実行すると、ApplicationController に入った before_action :require_authentication によって /session/new へリダイレクトされ、be_successful の検証が落ちます。認証ジェネレータを入れたアプリでは、scaffold の request spec はログイン処理を足すまで通りません。

もう1つの落とし穴が root ルートです。認証後の戻り先は Authentication concern の after_authentication_url が決めていて、保存された戻り先が無ければ root_url を使います。root を定義していないアプリでログインのPOSTを送ると、次のエラーで止まりました。

NameError:
  undefined local variable or method 'root_url' for an instance of SessionsController
# ./app/controllers/concerns/authentication.rb:38:in 'Authentication#after_authentication_url'

routes.rb に root "articles#index" のような行を足すと解消します。

rspec-rails が factory_bot_rails 向けに生成する spec/factories/users.rb は email_address { "[email protected]" } と固定値になっています。users テーブルの email_address には一意のインデックスが張られるため、同じ例の中で create(:user) を2回呼ぶと ActiveRecord::RecordNotUnique で失敗します。先に sequence へ書き換えておきます。

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    sequence(:email_address) { |n| "user#{n}@example.com" }
    password { "password" }
  end
end

sign_in_asヘルパーの2通りの書き方

1つ目は、ログイン画面と同じPOSTを実際に送る方法です。ログイン処理そのものも毎回通るので、認証まわりの変更にも気づけます。

# spec/support/session_helper.rb
module SessionHelper
  def sign_in_as(user)
    post session_path, params: { email_address: user.email_address, password: "password" }
  end
end

RSpec.configure do |config|
  config.include SessionHelper, type: :request
end

spec/support/ のファイルは既定では読み込まれません。rails_helper.rb にある Rails.root.glob('spec/support/**/*.rb') の行のコメントを外してください。生成された SessionsController には rate_limit to: 10, within: 3.minutes が付いていますが、テスト環境の cache_store は :null_store なので、1例の中で11回ログインしても弾かれませんでした。

2つ目は、Rails が Minitest 用に生成する session_test_helper.rb を spec/support/ にそのまま写す方法です。セッションを直接作り、署名付きCookieを書き込むので、ログインのPOSTを省けます。

# spec/support/session_test_helper.rb(Rails 8.1.4 のテンプレートと同じ内容)
module SessionTestHelper
  def sign_in_as(user)
    Current.session = user.sessions.create!

    ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
      cookie_jar.signed[:session_id] = Current.session.id
      cookies["session_id"] = cookie_jar[:session_id]
    end
  end

  def sign_out
    Current.session&.destroy!
    cookies.delete("session_id")
  end
end

ActiveSupport.on_load(:action_dispatch_integration_test) do
  include SessionTestHelper
end

末尾の ActiveSupport.on_load は Minitest の ActionDispatch::IntegrationTest 向けのフックです。それでも config.include を書かずに request spec から sign_in_as を呼べ、GET /articles は 200 を返しました。rspec-rails の RequestExampleGroup が ActionDispatch::IntegrationTest::Behavior を include しており、そこでこのフックが実行されるためです。どちらを選ぶかは、ログイン処理を spec で毎回通したいかで決めます。ログイン画面自体の spec を別に持っているなら、2つ目の方がリクエスト1回分速く済みます。

Deviseのsign_inをrequest specで使う設定

Devise を使うアプリなら、Devise が提供する Devise::Test::IntegrationHelpers を request spec に取り込みます。Devise の README は IntegrationTest への include と RSpec の feature spec への include しか例示していませんが、Devise 5.0.4 の検証アプリに次の設定を入れると、request spec でも sign_in が使えました。

# spec/rails_helper.rb
RSpec.configure do |config|
  config.include Devise::Test::IntegrationHelpers, type: :request
end

# spec/requests/dashboard_spec.rb
it "sign_in でログイン済みになる" do
  user = User.create!(email: "[email protected]", password: "password123")
  sign_in user
  get "/dashboard"
  expect(response.body).to eq "hello [email protected]"
end

Devise の認証戦略の土台である Warden の仕組みはWardenのカスタム戦略の作り方:valid?・authenticate!とDevise連携・JWT認証で解説しています。

shared_examplesによる未ログイン時の検証の共通化

「未ログインならログイン画面へ送る」という検証は、保護したいエンドポイントの数だけ並びます。shared_examples に切り出し、リクエストを subject に置くと、エンドポイントごとの記述は2行になります。

RSpec.shared_examples "ログイン画面へリダイレクトする" do
  it "未ログインなら /session/new へ送る" do
    subject
    expect(response).to redirect_to(new_session_path)
  end
end

RSpec.describe "Articles", type: :request do
  context "未ログイン" do
    describe "GET /articles" do
      subject { get articles_path }
      it_behaves_like "ログイン画面へリダイレクトする"
    end

    describe "POST /articles" do
      subject { post articles_path, params: { article: { title: "x" } } }
      it_behaves_like "ログイン画面へリダイレクトする"

      it "記事は作られない" do
        expect { subject }.not_to change(Article, :count)
      end
    end
  end

  context "ログイン済み" do
    let(:user) { create(:user) }
    before { sign_in_as(user) }

    it "一覧が 200 で表示される" do
      create(:article, title: "表示確認")
      get articles_path
      expect(response).to have_http_status(:ok)
      expect(response.body).to include("表示確認")
    end
  end
end

get を describe の直下に書くと、例の実行時ではなく spec の読み込み時に呼ばれてしまいます。リクエストは必ず it・before・subject のいずれかの中に置きます。

Rack 3.1以降で非推奨になった422のシンボル指定

バリデーションエラーの 422 は、Rack 3.1(2024年6月11日公開)で書き方が変わりました。Rails ではなく Rack の変更なので、Rack 3.1 以上を使っていれば Rails 7.2 でも 8.0 でも同じです。Rack 3.2.8 の lib/rack/utils.rb には、旧シンボルと新シンボルの対応表が定数で置かれています。

旧シンボル 現行シンボル ステータスコード
:unprocessable_entity :unprocessable_content 422
:payload_too_large :content_too_large 413

旧シンボルは今も 422 として動きます。ただし Rack 3.2.8 でも、have_http_status(:unprocessable_entity) を実行するたびに次の警告が標準エラー出力に出ました(実際は1行です)。

rspec-rails-8.0.4/lib/rspec/rails/matchers/have_http_status.rb:219: warning:
Status code :unprocessable_entity is deprecated and will be removed in a future version of Rack.
Please use :unprocessable_content instead.

テストが落ちないぶん、警告だけがCIのログに溜まり続けます。ジェネレータ側はシンボルを直接埋め込まず、rspec-rails の scaffold テンプレートは Rack::Utils::SYMBOL_TO_STATUS_CODE.key(422)、Rails のコントローラのテンプレートは ActionDispatch::Constants::UNPROCESSABLE_CONTENT を通して、生成時に入っている Rack から解決します。rspec-rails がこの方式に変わったのは 8.0.2(2025年8月12日)で、変更履歴に「Fix scaffold generator producing deprecated Rack http statuses.」とあります。既存コードの旧シンボルは grep -rn "unprocessable_entity" spec app で洗い出せます。使っている Rack の版は bundle list | grep rack で確認できます。

JSONレスポンス検証でのparsed_bodyとbodyの使い分け

構造を見たいなら response.parsed_body、文字列が含まれるかだけを見たいなら response.body を使います。parsed_body は Content-Type に応じてパース済みのオブジェクトを返し、JSONでは文字列キーでもシンボルキーでも値を引けました。JSON.parse(response.body) と書く必要はありません。

it "parsed_body は文字列キーでもシンボルキーでも引ける" do
  create(:article, title: "確認用")
  get "/api/articles"
  expect(response.parsed_body.first["title"]).to eq "確認用"
  expect(response.parsed_body.first[:title]).to eq "確認用"
end

it "response.body には日本語がそのまま入る" do
  create(:article, title: "日本語タイトル")
  get "/api/articles"
  expect(response.body).to include("日本語タイトル")
end

Rails 8.1 のJSON出力は日本語をユニコードエスケープしないので、include に日本語をそのまま渡して一致します。

日本語の解説で見かける expect(response.body).to be_json は動きません。be_json というマッチャは rspec-expectations 3.13.5 に無く、be_xxx の形は「対象の xxx? メソッドを呼ぶ」動的な述語マッチャとして解釈されます。String に json? は無いので、次のように失敗します。

Failure/Error: expect(response.body).to be_json
  expected "[{\"id\":1,\"title\":\"記事タイトル1\",...}]" to respond to `json?`

JSONとして正しいかを確かめたいなら、parsed_body が期待した型(Hash や Array)で返ることを検証します。モデルのバリデーションや関連付けを宣言的に確かめたい場合は、shoulda-matchersの使い方【8.0.1対応】設定・主要マッチャーと失敗例の直し方で扱っているgemの方が向いています。

FactoryBotによるテストデータ準備とsequenceの注意点

この記事のようにデータベースを使うエンドポイントの request spec では、テストデータの準備が spec の読みやすさを左右します。ファクトリは spec/factories/ に置きます。

# spec/factories/articles.rb
FactoryBot.define do
  factory :article do
    sequence(:title) { |n| "記事タイトル#{n}" }
    body { "本文サンプル" }
    published { true }

    trait :draft do
      published { false }
    end
  end
end

trait は既定値の一部だけを差し替える名前付きの変種で、create(:article, :draft) と呼べば published が false のレコードができます。公開・非公開でレスポンスが変わるエンドポイントの検証に使えます。

sequence の連番はファイル単位ではなく、実行中のスイート全体で共有されます。単独で実行すれば「記事タイトル1」から始まっても、他のファイルと一緒に流すと開始番号がずれ、config.order = :random では実行順によって番号が変わることがあります。同じseedで同じスイートを実行すれば、実行順を再現できます。sequence が作った文字列を期待値に直書きせず、値を検証したいときは create(:article, title: "更新前") のように明示します。ファクトリ全体が保存できるかは bin/rails runner -e test 'ActiveRecord::Base.transaction { FactoryBot.lint(traits: true); raise ActiveRecord::Rollback }' でまとめて確かめられます。

request spec・system spec・model specの守備範囲と実行時間の実測差

「request spec は system spec より速い」とはよく言われますが、差の大きさまで書いた解説はほとんどありません。Ruby 4.0.6 / Rails 8.1.4 / rspec-rails 8.0.4 / Capybara 3.40.0 / selenium-webdriver 4.52.0 で、「ログインしてから記事一覧に目的の記事が表示されることを確かめる」20例を3方式で書き、同じマシンで3回ずつ実行しました。表はRSpecの実行時間で、別に表示されるファイル読み込み時間は含みません。

実行方式 ドライバ 20例(3回の中央値) 3回の値
request spec なし 1.03秒 1.10 / 1.00 / 1.03秒
system spec rack_test 1.57秒 1.55 / 1.57 / 1.58秒
system spec headless Chrome 24.80秒 33.25 / 23.43 / 24.80秒

今回の比較では、ブラウザを使う構成で所要時間が大きく増えました。rack_test の system spec は request spec の約1.5倍で済み、ヘッドレスChromeにした途端に約24倍です。この差にはブラウザの起動に加え、画面操作・描画・通信の処理も含まれます。20例の中央値を単純に10倍すると200例相当で約10秒と約4分ですが、これは外挿で、固定費や処理内容によって実際の所要時間は変わります。

判断は次のとおりです。JavaScriptの実行結果や、ボタン・リンク操作を通した画面遷移は、ブラウザを使う system spec で確認します。一方、APIのレスポンス、認可の可否、リダイレクト先と遷移後のレスポンス、ステータスコードの分岐のように「HTTPの入出力で表せる仕様」は request spec を基本にします。今回の実測では、system spec の所要時間は rack_test で約1.5倍、ヘッドレスChromeで約24倍でした。迷ったら request spec を既定にし、ブラウザでしか再現できない不具合に出会ったときだけ system spec に上げてください。

model spec との切り分けは別の軸です。バリデーションやスコープのようにHTTPを介さず確かめられるロジックを request spec で検証すると、1つの振る舞いのために毎回リクエストを送ることになります。ロジックは model spec、経路と入出力は request spec という分担が保守しやすい形です。CIでテストを回す仕組みづくりから相談したい場合は、DevOps・CI/CD導入支援でも支援しています。

よくある質問

request specとcontroller specはどちらを書くべきですか?

新しく書くなら request spec です。rspec-rails 8.0.4 の README に、RSpec 3.5 の時点で Rails と RSpec の両チームがコントローラの直接テストを勧めなくなったと書かれています。controller spec は今も動きますが、ルーティングやミドルウェアを通らないため、検証できる範囲が狭くなります。

request specのファイルはどこに置き、type: :requestは必要ですか?

spec/requests/ に置きます。rspec-rails 8.0.4 が生成する rails_helper.rb では infer_spec_type_from_file_location! がコメントアウトされているので、type: :request を書かないと get などのメソッドが使えず NoMethodError になります。雛形は bin/rails generate rspec:request Articles で作れます。

422を期待するとき、unprocessable_entityとunprocessable_contentのどちらを使いますか?

Rack 3.1 以上なら :unprocessable_content です。:unprocessable_entity も 422 として解釈されますが、Rack が「将来の版で削除される」という警告を実行のたびに出します。決め手は Rails ではなく Rack の版で、bundle list | grep rack で確認できます。

request specでログイン済みの状態を作るにはどうしますか?

Rails 8 の認証ジェネレータなら、post session_path でログインのPOSTを送るヘルパーを spec/support/ に置くか、Rails が Minitest 用に生成する session_test_helper.rb を写します。Devise なら config.include Devise::Test::IntegrationHelpers, type: :request を書けば sign_in user が使えます。どちらも Rails 8.1.4 で動作を確認しました。

RSpecとrspec-railsは何が違いますか?

RSpec は Ruby のテストフレームワーク本体(2026年11月時点で rspec-core 3.13.6)、rspec-rails はそれを Rails に組み込み、request spec や model spec などの種類と専用マッチャを足すgem(8.0.4)です。Rails アプリでは rspec-rails を Gemfile に書けば、RSpec 本体も依存として入ります。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.03 テックブログ クラブツーリズム不正アクセス:約432万件の対象確認と偽メールの見分け方・流出検知の設計
  2. 2026.11.04 テックブログ CVE-2026-119977:WordPress更新中のRCEと安全な更新手順
  3. 2026.10.29 テックブログ ソニー生命の情報漏洩38.5万件|ログインできない理由、再開状況と窓口の電話番号
  4. 2026.11.02 テックブログ アララの不正アクセス:メール約7,700万件の持ち出しと契約企業が行うAPIキー再設定・代替送信・連名報告
  5. 2026.11.03 テックブログ ノジマ不正アクセスで約700万件漏えい:対象確認・窓口と乗っ取り注文を止める実装

RELATED POSTS 関連記事

目次