Ruby on Rails

request specとは?Rails 8・RSpec 8でのリクエストスペックの書き方

request specとは?Rails 8・RSpec 8でのリクエストスペックの書き方

request spec(リクエストスペック)は、HTTPリクエストを1本投げてHTTPレスポンスを検証する、Rails標準の結合テストです。コントローラのメソッドを直接呼ぶのではなく、ルーティングからミドルウェア、コントローラ、モデル、レスポンス生成までを実際に通します。この記事では Rails 8.1.3.1 と rspec-rails 8.0.4 を実際に動かし、通ることを確認したコードだけを掲載しました。あわせて、Rack 3.1 以降で 422 のシンボル指定が変わった点や、request spec と system spec の実行時間の差など、2022年前後に書かれた解説では触れられていない現行仕様も扱います。

まとめ

  • request spec は「HTTPリクエストを投げ、HTTPレスポンスを検証する」テスト。RSpec 3.5 以降、RSpec と Rails の両チームがコントローラの直接テストを非推奨とし、その代替として位置づけている。
  • 置き場所は spec/requests/。bin/rails generate rspec:install で土台を作り、bin/rails generate rspec:request Articles で雛形を出す。
  • 422 のシンボルは Rack 3.1(2024年6月)で :unprocessable_entity から :unprocessable_content へ移行した。Rails 8.1 固有の変更ではなく、Rack 3.1 以上を積む Rails 7.2 系・8.0 系でも同じ。旧シンボルも動くが、実行のたびに Rack の非推奨警告が出る。
  • JSONの検証は response.parsed_body を使う。be_json というマッチャは RSpec に存在せず、書くとエラーになる。
  • 20例を同条件で実行すると request spec 0.41秒、実ブラウザ駆動の system spec 19.3秒。約47倍の差が、テストの粒度を決める判断材料になる。
  • ファイル監視による自動実行を担ってきた guard-rspec は2016年で更新が止まっている。失敗したテストの再実行だけなら RSpec 標準の --only-failures で足りる。

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

RSpecの公式ドキュメントは request spec を「Railsの結合テストへの薄いラッパーであり、ルーティングを含むフルスタックを、スタブを挟まずに駆動するもの」と説明しています。ここで重要なのは「ルーティングを含む」という点です。コントローラのアクションを直接呼ぶテストでは、ルーティング定義の誤りやミドルウェアの挙動は検証範囲の外に落ちます。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 へ寄せるのが素直な判断です。

RSpec そのものの導入や、model spec を含めた全体像は RSpecとは?Rubyのテスト自動化の書き方とrspec-rails導入・RSpec 4移行を解説で扱っています。

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

rspec:installで生成されるファイル

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 の後にインストールジェネレータを実行すると、設定ファイル3点と spec ディレクトリが作られます。

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

request spec が読み込むのは spec/rails_helper.rb です。FactoryBot の create や create_list をテスト内でそのまま呼べるようにするため、次の設定を加えておきます。この一行を忘れると NoMethodError になります。

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

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

ファイルは spec/requests/ に置きます。空のファイルを手で作る必要はなく、専用のジェネレータがあります。

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

生成されるのは RSpec.describe "Comments", type: :request do の枠と例1本だけです。その1本は get comments_index_path を呼ぶため、対応するルートを用意するまでは NameError で落ちます。動作するサンプルではなく、書き換えを前提にした雛形と考えてください。

一方、bin/rails generate scaffold を使うと、rspec-rails が index / show / create / update / destroy の5アクション分の request spec を一気に出力します。こちらは valid_attributes と invalid_attributes が skip で空けてあり、そこを埋めれば動く構造になっています。ゼロから書くより、scaffold の出力を読んで書き方の型を掴むほうが早道です。

HTTPメソッド別の書き方とレスポンス検証

以下のコードは、title に presence バリデーションを持つ Article モデルと、Rails 8.1 の scaffold が生成した API モードのコントローラに対して実行し、この章の6例が 0 failures で通ることを確認したものです。

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

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

require "rails_helper"

RSpec.describe "Articles API", type: :request do
  describe "GET /articles" do
    it "200 と登録済みの記事一覧を返す" do
      create_list(:article, 3)

      get "/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 のようなカテゴリも渡せます。カテゴリ指定は「2xxのいずれか」という緩い検証になるため、ステータスコードを仕様として固定したい場面ではシンボルか数値を使います。

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

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

describe "POST /articles" do
  it "記事が1件増え、201 を返す" do
    expect {
      post "/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
end

as: :json は Content-Type を application/json にし、パラメータをJSONとして送るオプションです。省略してもフォーム形式として送られ、上記のコントローラでは同じく 201 が返りました。JSON APIを検証する意図を明示する目的で付けておくと、後から読む人に伝わりやすくなります。

異常系では「増えないこと」を not_to change で確認します。増加数を by(0) と書くこともできますが、意図が読み取りやすいのは not_to のほうです。

it "title が空なら 422 を返し、記事は増えない" do
  expect {
    post "/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

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

更新系で漏れやすいのが reload です。テスト内の変数はリクエスト前にメモリへ読み込まれたオブジェクトなので、リクエストで更新してもそのままでは古い値を保持しています。article.reload.title とデータベースから読み直して初めて、更新が本当に永続化されたかを確認できます。

describe "PATCH /articles/:id" do
  it "title が書き換わる" do
    article = create(:article, title: "更新前")

    patch "/articles/#{article.id}", params: { article: { title: "更新後" } }, as: :json

    expect(response).to have_http_status(:ok)
    expect(article.reload.title).to eq "更新後"
  end
end

DELETEリクエストでのレコード減少とno_content判定

削除は change(...).by(-1) で件数の減少を確認します。Rails 8.1 の scaffold が生成する API コントローラの destroy は本文を返さないため、ステータスは :no_content(204)です。:ok を期待して書くと落ちます。

describe "DELETE /articles/:id" do
  it "記事が1件減り、204 を返す" do
    article = create(:article)

    expect {
      delete "/articles/#{article.id}"
    }.to change(Article, :count).by(-1)

    expect(response).to have_http_status(:no_content)
  end
end

存在しないIDへのアクセスも request spec で確認できます。ActiveRecord::RecordNotFound は Rails が 404 に変換するため、例外ではなくステータスとして検証します。

it "存在しない記事は 404 を返す" do
  get "/articles/999999"

  expect(response).to have_http_status(:not_found)
end

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

バリデーションエラーの検証は request spec で最も書く機会の多い箇所ですが、ここは Rack 3.1(2024年6月11日リリース)で書き方が変わりました。Rails 側の変更ではないため、Rack 3.1 以上を積んでいれば Rails 7.2 系でも 8.0 系でも同じ話になります。Rack のソース(lib/rack/utils.rb)には、廃止予定のシンボルと新しいシンボルの対応表が定数として置かれています(Rack 3.0 系には存在しません)。

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

旧シンボルは今も動作します。ただし Rack の status_code が警告を出すため、実行するたびに次のメッセージが標準エラー出力に流れます(実際には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.

ジェネレータ側の対応も済んでいますが、どちらもシンボルを直接埋め込んではいません。rspec-rails の scaffold テンプレートは Rack::Utils::SYMBOL_TO_STATUS_CODE.key(422)、Rails の scaffold コントローラのテンプレートは ActionDispatch::Constants::UNPROCESSABLE_CONTENT を通し、生成時にインストール済みの Rack から解決します。出力されるシンボルは Rails ではなく Rack の版で決まるということです。Rack 3.1 以上なら、コントローラ側の status: :unprocessable_content と spec 側の have_http_status(:unprocessable_content) が生成されます。この方式へ切り替わったのは rspec-rails 8.0.2(2025年8月12日)で、変更履歴に「Fix scaffold generator producing deprecated Rack http statuses.」と記録されています。

やっかいなのは、旧シンボルを書いてもテストが落ちない点です。422 として解釈されるので結果は変わらず、警告だけが CI のログに溜まり続けます。2022年前後に書かれた日本語の解説記事はこの変更より前の記述のままなので、参考にするときは読み替えが要ります。既存のコードは grep -rn "unprocessable_entity" spec app で一括して洗い出せます。

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

レスポンス本文の検証には response.body と response.parsed_body の2つがあります。使い分けの基準は単純で、構造を見たいなら parsed_body、文字列として含まれるかだけを見たいなら body です。

response.parsed_body は Content-Type に応じてパース済みのオブジェクトを返します。Rails 8.1 で実行したところ、JSONレスポンスでは文字列キーとシンボルキーのどちらでも値を引けました。JSON.parse(response.body) と書く必要はありません。

it "parsed_body は文字列キーでもシンボルキーでも引ける" do
  create(:article, title: "確認用")

  get "/articles"

  expect(response.parsed_body.first["title"]).to eq "確認用"
  expect(response.parsed_body.first[:title]).to eq "確認用"
end

response.body を使う場合、日本語の扱いが気になるところですが、Rails 8.1 のJSONレンダリングは非ASCII文字をユニコードエスケープしません。実際のレスポンス本文には "title":"日本語タイトル" がそのまま入るため、include マッチャに日本語をそのまま渡して一致します。

it "response.body には日本語がそのまま入る" do
  create(:article, title: "日本語タイトル")

  get "/articles"

  expect(response.body).to include("日本語タイトル")
end

日本語の解説記事には expect(response.body).to be_json のような「JSON形式かどうかを判定するマッチャ」が書かれていることがありますが、be_json というマッチャは RSpec に存在しません。rspec-expectations 3.13.5 の組み込みマッチャ一覧に該当するファイルはなく、実行すると RSpec の動的プレディケート機能に解釈され、次のように失敗します。

Failure/Error: expect(response.body).to be_json
  expected "[{\"id\":1,...}]" to respond to `json?`

be_xxx と書くと RSpec は対象オブジェクトの xxx? メソッドを呼ぼうとします。String に json? は無いため、マッチャが無いことすら分かりにくいエラーになります。JSONとして妥当かを確認したいなら、parsed_body が期待する型(Hash や Array)で返ることを検証するのが確実です。モデルのバリデーションやアソシエーションを宣言的に検証したい場合は、shoulda-matchersの使い方【8.0対応】|設定・主要マッチャー・バージョン選定で扱っている専用の gem が向いています。

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

request spec は毎回データベースを経由するため、テストデータの用意が本体と同じくらい重要になります。FactoryBot では 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 のレコードが作られるため、公開・非公開でレスポンスが変わるエンドポイントの検証がそのまま書けます。1件だけ作るなら create(:article)、複数件なら create_list(:article, 3) を使います。

sequence には落とし穴があります。連番のカウンタはテストファイル単位ではなく、実行中のスイート全体で共有されるものです。単独で実行すれば「記事タイトル1」から始まりますが、他のファイルと一緒に実行すると開始番号がずれます。手元でも、単独なら通る eq "記事タイトル1" が、ファイルを1本追加した途端に別の番号を受け取って落ちました。何番になるかは同時に走るファイルの数と順序次第で、config.order = :random を有効にしていれば実行のたびに変わります。sequence が生成した具体的な文字列を期待値に直書きしてはいけません。値そのものを検証したいときは create(:article, title: "更新前") のように明示的に指定してください。

ファクトリが増えてきたら、全ファクトリが実際に保存できるかをまとめて検査できます。バリデーション追加後の取りこぼしを見つける用途に有効です。

$ bin/rails runner -e test 'FactoryBot.lint'

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

「request spec は system spec より速い」という説明はよく見かけますが、どれくらい速いのかは書かれていないことがほとんどです。Ruby 4.0.6 / Rails 8.1.3.1 / rspec-rails 8.0.4 の環境で、単一リソースのAPIアプリに対し、同一マシン・同一の20例・同等のアサーションで測った結果が次のとおりです。

実行方式 ドライバ 20例の実行時間
request spec なし 0.41秒
system spec rack_test 0.47秒
system spec headless Chrome 19.3秒

この数字は、速度差の正体が「specの種類」ではなくブラウザを起動するかどうかであることを示しています。rack_test ドライバの system spec は request spec とほぼ変わらず、実ブラウザを立ち上げた途端に約47倍になりました。1例あたり約0.02秒と約0.97秒の違いは、テストが200例あれば4秒と3分13秒の違いになります。

使い分けの判断はここから決まります。JavaScriptの実行結果や画面遷移が仕様の中心にある画面は system spec でしか検証できず、実ブラウザのコストを払う価値があります。一方、APIのレスポンス、認可の可否、リダイレクト先、ステータスコードの分岐といった「HTTPの入出力で表現できる仕様」を system spec で書くと、得られる情報は変わらないまま実行時間だけが二桁増えます。迷ったら request spec を既定にし、ブラウザでしか再現できない事象に出会ったときだけ system spec に上げてください。

model spec との切り分けは別の軸です。バリデーションやスコープなど、HTTPを介さずに確認できるロジックを request spec で検証すると、1つの振る舞いを確認するために毎回リクエストのオーバーヘッドを払うことになります。ロジックは model spec、経路と入出力は request spec という分担が保守しやすい形です。モデル側が肥大してテストを書きにくくなっている場合は、RailsのFat Model対策|1500行model.rbを分割する4手法と判断基準で扱っている分割の考え方が先に要ります。

テンプレートの描画結果だけを確かめる view spec も rspec-rails には残っていますが、描画までを含めて確認したい場面は request spec か system spec で賄えることが多く、新規に選ぶ場面は限られます。なお rspec.info の公式ドキュメントが明記しているとおり、Capybara のDSLは request spec ではサポートされません。ブラウザ操作を伴う検証の構築は RSpecでCapybaraとPlaywrightを導入するための基本手順を参照してください。

guard-rspec停止後のテスト再実行手段

ファイル保存を検知してテストを自動実行する仕組みとして、長らく guard-rspec が紹介されてきました。ただし RubyGems 上の最新版は 4.7.3 で、公開日は 2016年7月29日です。10年近く新しいリリースが出ていない gem を、Rails 8 系のプロジェクトに新規導入する判断は勧められません。ファイル監視そのものが目的なら、Ruby に依存しない汎用ツール(entr、watchexec など)に bundle exec rspec を渡す形のほうが、依存を増やさずに済みます。

そもそも「保存のたびに全件を回す」必要がある場面は多くありません。失敗したテストだけを再実行したいという目的であれば、RSpec 本体の機能で足ります。--only-failures は前回の実行結果を記録したファイルを参照し、失敗した例だけを選んで走らせるオプションです。使うには実行結果の保存先を設定する必要があります。

# spec/spec_helper.rb
RSpec.configure do |config|
  config.example_status_persistence_file_path = "tmp/rspec_status.txt"
end

設定せずに実行すると To use --only-failures, you must first set config.example_status_persistence_file_path. と表示されて止まります。設定後は、2例中1例が失敗した状態から再実行すると対象が1例に絞られます。

$ bundle exec rspec
2 examples, 1 failure

$ bundle exec rspec --only-failures
Run options: include {last_run_status: "failed"}
1 example, 1 failure

保存先には各例の実行結果と所要時間が記録されるため、どの例が遅いのかを把握する材料にもなります。

example_id                         | status | run_time        |
---------------------------------- | ------ | --------------- |
./spec/requests/flaky_spec.rb[1:1] | passed | 0.1432 seconds  |
./spec/requests/flaky_spec.rb[1:2] | failed | 0.15674 seconds |

失敗を1件ずつ順に潰したい場面では --next-failure も使えます。こちらは失敗した最初の1例だけを実行して止まるため、修正と再実行を細かく往復する作業に向いています。

よくある質問

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

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

request specのファイルはどこに置きますか?

spec/requests/ です。bin/rails generate rspec:request Articles を実行すると spec/requests/articles_spec.rb が生成されます。ファイル冒頭で require "rails_helper" し、RSpec.describe "Articles", type: :request do の形で書き始めます。

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

Rack 3.1 以上なら :unprocessable_content です。:unprocessable_entity も422として解釈されますが、Rack が「将来のバージョンで削除される」という非推奨警告を実行のたびに出力します。Rails の版ではなく Rack の版で決まる点に注意してください。bundle list | grep rack で確認できます。

JSONレスポンスの中身はどう検証しますか?

response.parsed_body を使います。Content-Type に応じてパース済みのオブジェクトが返り、文字列キーでもシンボルキーでも値を引けます。JSON.parse(response.body) と書く必要はありません。be_json というマッチャは存在しないため、書くと respond_to `json?` のエラーで失敗します。

非同期ジョブが登録されたことはrequest specで確認できますか?

確認できます。rspec-rails が提供する have_enqueued_job マッチャを expect { post "/articles" }.to have_enqueued_job(SomeJob) の形で使えば、リクエスト処理の中でジョブがキューへ積まれたかを検証できます。ジョブ側の実装やリトライ設定は Rails Active Jobとは?非同期ジョブ処理の実装とSolid Queue・リトライ設定の担当範囲です。

関連記事

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

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

資料請求

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

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.08 テックブログ 大阪公立大学のランサムウェア被害と仮想化基盤の停止|全授業休講に至った経緯とバックアップを守る設定
  3. 2024.11.08 テックブログ OpenAPI GeneratorでJavaコードを自動生成する方法|CLI導入からSpring・ライブラリ選択まで
  4. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  5. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動

RELATED POSTS 関連記事

目次