RSpecは、Rubyのコードに対する自動テストを describe・context・it という入れ子で記述するテストフレームワークです。2026年8月時点の安定版は rspec 3.13.2、Rails向けの rspec-rails は 8.0.4。一方で2026年2月18日には should 構文の削除を含む RSpec 4.0.0.beta1 が公開され、いま書くテストコードの書き方にも影響が出ています。この記事では導入手順から構文、マッチャー、実行の絞り込み、そして4.0への移行判断までをまとめます。
まとめ
RSpecは「振る舞いを日本語に近い形で記述し、そのままテストとして実行する」ことに寄せたフレームワークです。Rails標準のMinitestと機能が重なるため、選ぶ理由はチームがどれだけ let・subject・shared_examples による構造化を必要とするかで決まります。導入は Gemfile に rspec-rails を足して rails generate rspec:install を実行するだけで、生成される spec/rails_helper.rb と spec/spec_helper.rb の役割分担さえ押さえれば運用に乗ります。
いま新規に書くなら、3.13系であっても should 系と素の describe(グローバルDSL)は使わないでください。RSpec 4.0.0.beta1 で両方とも削除済みで、RSpec.describe と expect だけで書いておけば移行コストがほぼゼロになります。RSpecを何のために入れるのかという判断そのものはテスト駆動開発(TDD)の基礎側で扱っているので、そちらとあわせて読んでください。以下、守備範囲とMinitest比較・導入・構文・マッチャー・実行制御・4.0差分の順に見ていきます。
RSpecの守備範囲とMinitestとの選択基準
RSpecがRubyのテスト自動化で担う層
RSpecが受け持つのは、テストの記述構文・実行ランナー・検証(マッチャー)・テストダブルの4つです。gemも役割ごとに分かれていて、rspec 3.13.2 は rspec-core(ランナーと describe 系DSL)、rspec-expectations 3.13.5(expect とマッチャー)、rspec-mocks 3.13.8(スタブとモック)をまとめて入れるメタgemにあたります。
逆に、RSpec単体では守備範囲外のものもはっきりしています。ブラウザ操作は Capybara や Playwright、カバレッジ計測は SimpleCov、Railsのモデル検証の記述短縮は shoulda-matchers といった別gemの担当です。RSpecはそれらを載せる土台と考えてください。ブラウザ側の組み合わせはRSpecでCapybaraとPlaywrightを導入するための基本手順、モデル検証の短縮記法はshoulda-matchersの使い方で扱っています。
MinitestとRSpecの判断軸
Railsが標準で生成するテストはMinitestです。MinitestはRubyのbundled gemとして配布物に含まれており、Ruby 4.0.6 には minitest 6.0.0、まだ現役の Ruby 3.4系には 5.25.4 が同梱されます(RubyGems上の最新は 6.0.6・2026年5月1日公開)。追加のgemなしで動く分、導入コストはゼロです。それでもRSpecを選ぶかどうかは、次の観点で見比べてください。
| 観点 | RSpec 3.13系 | Minitest(Ruby同梱) |
|---|---|---|
| 導入 | rspec-rails を追加 | Rails標準・追加なし |
| 記述 | describe / context / it の入れ子 | クラスとメソッド |
| 前提の共有 | let / subject / shared_examples | setup とヘルパーメソッド |
| 検証 | expect + マッチャー | assert_* 系 |
| 実行の絞り込み | --only-failures / --bisect |
-n(名前指定)中心 |
判断はこうです。条件分岐が多く、同じ前提で入力だけを変えたケースを何十本も書く領域(決済、権限、料金計算など)ではRSpecの context と shared_examples が効きます。一方、テスト本数が数十本規模のgemや小さな社内ツールにRSpecを入れるのは過剰です。DSLの学習コストと依存gemが増えるだけで、Minitestの assert_equal で十分足ります。既存プロジェクトが片方で書かれているなら、混在させずそちらに揃えてください。
RSpecの導入手順とrspec-rails 8系のバージョン要件
素のRubyプロジェクトへの導入
Rails以外のRubyプロジェクトでは rspec を開発用グループに入れます。Bundlerでgemの依存関係を管理している前提で、Gemfileに次の1行を足します。
group :development, :test do
gem 'rspec', '~> 3.13'
end
bundle install のあと bundle exec rspec --init を実行すると、実行時オプションを書く .rspec と、共通設定を書く spec/spec_helper.rb が生成されます。テストファイルは spec/ 配下に _spec.rb で終わる名前で置くのが既定の探索パターンです。
Railsプロジェクトへの導入とRails 7.2以上という要件
Railsでは rspec ではなく rspec-rails を入れます。最新は 8.0.4(2026年3月11日公開)で、runtime依存として actionpack・activesupport・railties にいずれも >= 7.2 を要求します。つまりRails 7.1以前のアプリでは 8.0系は入りません。その場合は rspec-rails 7.1.1 以前を選ぶことになります。
group :development, :test do
gem 'rspec-rails', '~> 8.0'
end
導入コマンドは rails generate rspec:install です。公式READMEが示すとおり、.rspec・specディレクトリ・spec/spec_helper.rb・spec/rails_helper.rb の4つが作られます。
$ rails generate rspec:install
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
この2つのヘルパーは役割が違います。spec_helper.rb はRailsに依存しないRSpec自体の設定、rails_helper.rb は先に spec_helper を読んだうえでRails環境とActiveRecordのトランザクション制御を追加で読み込みます。Railsのモデルやコントローラを触るspecでは rails_helper を、純粋なPORO(Railsに依存しないクラス)のspecでは spec_helper だけを requireしてください。後者はRails環境のロード(数百ミリ秒から数秒)がまるごと消えます。
type メタデータで決まるspecの種類
rspec-rails は type: というメタデータを見て、そのspecに追加の振る舞いを混ぜ込みます。type: :request なら get や post が呼べる、type: :system ならCapybaraのDSLが使える、といった具合です。rspec-rails 8.0.4 の DIRECTORY_MAPPINGS には13種類が定義されています。
| type | 置き場所 | 主な用途 |
|---|---|---|
| :model | spec/models | バリデーション・スコープ |
| :request | spec/requests | HTTPレベルの検証 |
| :system | spec/system | ブラウザ操作 |
| :job | spec/jobs | ActiveJob |
| :mailer | spec/mailers | メール本文・宛先 |
| :helper | spec/helpers | ビューヘルパー |
ここに落とし穴があります。ディレクトリ名からtypeを自動判定する config.infer_spec_type_from_file_location! は、生成される rails_helper.rb ではコメントアウトされたままです。しかもテンプレート内のコメントは、この location ベースの推論を「legacy であり将来のバージョンで削除される」と明言しています。spec/requests に置いたのに get が呼べない、という詰まり方をしたら原因はこれです。自動判定を有効化するのではなく、各specに RSpec.describe UsersController, type: :request do と明示してください。HTTPレベルの検証の書き方そのものはリクエストスペックの基本概念と目的、ブラウザ操作はCapybaraとPlaywrightの導入手順で扱っています。
describe・context・itで組み立てるテストの構造
describeとcontextの書き分け基準
3つのブロックの役割は明確に分かれます。describe は「何をテストするか」(クラスやメソッド)、context は「どういう状況か」(前提条件)、it は「どうあるべきか」(期待する1つの振る舞い)です。
RSpec.describe Order do
describe '#total' do
context '商品が2件ある場合' do
it '合計金額を返す' do
order = Order.new(items: [Item.new(price: 300), Item.new(price: 700)])
expect(order.total).to eq(1000)
end
end
end
end
先頭が RSpec.describe であって素の describe でない点に注意してください。RSpec 3系ではルートスコープの describe も動きますが、これはグローバルDSLと呼ばれる仕組みで、RSpec 4.0.0.beta1 では削除済みです。慣習として describe にはクラス名、メソッド名を書くときはインスタンスメソッドに #、クラスメソッドに . を付けます。context の文字列は「〜の場合」で揃えてください。失敗時の出力がそのまま仕様書として読めるようになります。
letとsubjectによる前提データの共有
同じ前提を各 it にコピーすると、仕様変更のたびに全ケースを直すことになります。let は遅延評価される名前付きの値、subject はテスト対象そのものを指す特別な let です。内側の context で同名の let を再定義すると上書きされるため、差分だけを書けます。
RSpec.describe Order do
subject(:order) { Order.new(items: items) }
let(:items) { [Item.new(price: 300), Item.new(price: 700)] }
it '合計金額を返す' do
expect(order.total).to eq(1000)
end
context '商品が空の場合' do
let(:items) { [] }
it '0を返す' do
expect(order.total).to eq(0)
end
end
end
let は最初に参照された時点で評価され、同一のexample内では結果が使い回されます。参照されなければ評価されません。副作用のあるレコード生成をどのexampleでも必ず走らせたい場合だけ let! を使ってください。ただし let! を多用すると「使われていないデータを毎回作る」状態になり、実行時間がじわじわ伸びます。
値ではなく処理そのものを先に走らせたいときは before フックの出番です。before(スコープ既定は :each)は各exampleの直前、after は直後に実行され、複数書けば定義順に積み上がります。住み分けの基準はこうです。参照されて初めて要る値は let、参照の有無にかかわらず必ず起こしたい副作用は before。let! は before の中で let を呼ぶだけの糖衣なので、意図が副作用寄りなら最初から before と書いた方が読み手に伝わります。
RSpec.describe Order do
let(:items) { [Item.new(price: 300)] }
before { Rails.cache.clear }
after { ActionMailer::Base.deliveries.clear }
it '合計金額を返す' do
expect(Order.new(items: items).total).to eq(300)
end
end
スコープに :all(別名 :context)を指定した before(:all) だけは扱いが違います。グループ全体で1回しか走らず、Railsのspecでは各exampleを包むトランザクションの外側で動くため、ここで作ったレコードはロールバックされずDBに残ります。rspec-rails 8.0.4 の fixture_support.rb にも、このスコープでfixtureメソッドを呼ぶと警告を出すコードが入っています。Railsでは原則使わないでください。
複数のクラスが同じ振る舞いを持つとき、期待の記述をまとめて共有できます。定義に RSpec.shared_examples、呼び出しに it_behaves_like を使います。
RSpec.shared_examples '公開済みリソース' do
it 'published? が true を返す' do
expect(resource.published?).to be(true)
end
end
RSpec.describe Article do
let(:resource) { Article.new(state: 'published') }
it_behaves_like '公開済みリソース'
end
呼び出しメソッドには it_behaves_like と include_examples の2つがあり、前者は入れ子のグループを作るので let の上書きが共有側に届き、後者は現在のグループに直接展開されるため名前衝突を起こしやすくなります。迷ったら it_behaves_like を選んでください。なお、古い記事で見かける it_should_behave_like は RSpec 4.0.0.beta1 で削除された別名です。いま書くなら使わないでください。
expectとマッチャーの使い分け
等価性・包含・述語の各マッチャー
検証は expect(実際の値).to マッチャー の形で書きます。よく使うものは限られていて、実務では次の4系統でほとんど足ります。
expect(order.total).to eq(1000) # == で比較
expect(order.items).to include(item) # 配列やハッシュの包含
expect(order).to be_paid # order.paid? が真
expect(response).to have_attributes(status: 200)
eq は ==、eql は eql?、equal(および be)は equal?=同一オブジェクト判定に対応します。数値やnil、真偽値の比較で eq と be を混ぜると意図がぼやけるので、値の比較は eq、nil や true の同一性判定は be と決めておくと読みやすくなります。be_paid のような述語マッチャーは、対象に paid? があれば自動で解決される仕組みです。
ハッシュの一部だけを検証したい場合も include で書けます。hash_including という似た名前のマッチャーもありますが、あれは rspec-mocks が引数照合に使うものなので、expect 側では include に寄せてください。
expect(response.headers).to include('Content-Type' => 'application/json')
例外とブロックを対象にするマッチャー
例外の発生や状態の変化を検証するときは、引数ではなくブロックを expect に渡します。
expect { order.cancel! }.to raise_error(Order::AlreadyShipped)
expect { order.pay! }.to change(order, :state).from('pending').to('paid')
expect { order.pay! }.to change { Payment.count }.by(1)
raise_error は引数なしでも書けますが、その場合はNoMethodErrorのような想定外の例外まで通してしまいます。rspec-expectations は引数なしの raise_error に対して偽陽性の警告を出す設計になっているので、例外クラスかメッセージのどちらかは必ず指定してください。
rspec-mocksによるスタブとメッセージ期待
外部APIや通知のように、テスト中に実行したくない処理は rspec-mocks で差し替えます。値を返すだけなら allow、呼ばれたこと自体を検証するなら expect ... to have_received を使います。
allow(mailer).to receive_messages(deliver_now: true, deliver_later: true)
allow(gateway).to receive(:charge).and_return(true)
order.pay!
expect(gateway).to have_received(:charge).with(amount: 1000)
receive_messages は複数メソッドの戻り値をハッシュでまとめて定義する記法です。ここで既定値をひとつ押さえてください。rspec-mocks 3.13.8 のソースでは Configuration#initialize が @verify_partial_doubles = false で始まり、実在しないメソッドをスタブしてもエラーになりません。実装側のメソッド名を変えたのにテストだけ通り続ける事故は、ここから生まれます。
救いは、rspec --init と rails generate rspec:install が生成する spec/spec_helper.rb に mocks.verify_partial_doubles = true が最初から書かれている点です(rails_helper.rb 側には入っていません)。危ないのは生成物を使わず RSpec.configure を自分で書き起こしたときで、その場合だけ明示的に true を足す必要があります。
カスタムマッチャーを定義する判断基準
独自のマッチャーは RSpec::Matchers.define で作れます。ただし作る前に一度立ち止まってください。
RSpec::Matchers.define :be_within_business_hours do
match do |time|
(9..18).cover?(time.hour) && !time.saturday? && !time.sunday?
end
end
カスタムマッチャーが割に合うのは、同じ判定が複数のspecファイルに散っていて、かつ失敗時のメッセージを読みやすくしたい場合だけというのが筆者の判断基準です。1〜2箇所でしか使わない判定は普通のヘルパーメソッドで書いた方が、他のメンバーが定義を探す手間を減らせます。RailsのモデルバリデーションのようにDSL化の価値が高い領域は、自作せずshoulda-matchersのような既製gemに寄せる方が保守が楽です。
テスト実行の絞り込みと失敗テストの切り分け
失敗したテストだけを再実行する設定
rspec-core の option_parser には --only-failures(前回失敗したexampleだけを実行)と -n/--next-failure(--only-failures --fail-fast --order defined と等価)が用意されています。
ただし、このオプションは設定なしでは動きません。Configuration には only_failures_but_not_configured? という判定があり、example_status_persistence_file_path が未設定なら実行結果を保存する先が無いためエラーになります。spec_helper.rb に次の1行を入れておいてください。
RSpec.configure do |config|
config.example_status_persistence_file_path = 'tmp/rspec_examples.txt'
end
実行順序の乱数化と壊れたテストの特定
テストが単体では通るのに全体では落ちる場合、原因はexample間の状態の持ち越しです。--order random:12345 のようにシードを固定して順序を再現し、--bisect で原因となるexampleの組み合わせを自動的に絞り込みます。
bundle exec rspec --order random:12345
bundle exec rspec --bisect
bundle exec rspec --only-failures
rspec-core 3.13.6 のソースでは Ordering::Registry が register(:global, identity) となっており、既定の実行順は定義順です。つまり、明示的に乱数順を指定しない限り、順序依存のテストは何年でも隠れたままになります。CIでは乱数順を既定にしておくことをおすすめします。
GitHub Actionsでの実行と並列化の判断
CIに載せる最小構成は次のとおりです。参照するアクションはメジャータグで指定します。ruby/setup-ruby は週次でパッチが出る(2026年8月時点の最新は v1.321.0・7月22日公開、actions/checkout は v7.0.1・7月20日公開)ため、パッチ版まで固定するとワークフローの更新が終わりません。
name: rspec
on: [push]
jobs:
spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bundle exec rspec
bundler-cache: true を付けると bundle install の結果がキャッシュされ、毎回のgemインストールが省けます。並列化に手を出すのはその後です。parallel_tests のようなジョブ分割は、ランナー起動とbundle復元のオーバーヘッドに加えてテスト用DBを分ける手間がかかるため、その固定費を上回るだけの実行時間になってから入れてください。どの範囲までテストできているかの判断はテストカバレッジのC0/C1/C2と計測ツールを目安にしてください。
RSpec 4.0で消える書き方と3.13系からの移行判断
4.0.0.beta1で削除された構文
RSpec 4.0.0.beta1 は2026年2月18日に公開されました。rspec-core の gemspec は required_ruby_version = '>= 3.0.0' となっており、3.13.6 の '>= 1.8.7' から一気に引き上げられています。Changelogに並ぶ破壊的変更のうち、既存のテストコードに直接影響するものを挙げます。
| 変更点 | 3.13系 | 4.0.0.beta1 |
|---|---|---|
| should / should_not | 使用可 | 削除 |
| should_receive / stub | 使用可 | 削除 |
| ルートスコープのdescribe | 使用可 | 削除 |
| it_should_behave_like | 使用可 | 削除 |
--color オプション |
使用可 | 削除 |
| 既定の実行順 | defined | random |
| verify_partial_doubles | false | true |
| strict_predicate_matchers | false | true |
影響が大きいのは下3行です。実行順が乱数既定になると、これまで定義順に依存して通っていたテストが落ち始めます。verify_partial_doubles の既定有効化で引っかかるのは、存在しないメソッドへのスタブ。strict_predicate_matchers が既定になると、be_paid のような述語マッチャーが paid? の戻り値に true/false そのものを要求するため、真値を返しているだけのコードは失敗します。
なお --color の削除はカラー出力の廃止ではありません。--force-color と --no-color は4.0.0.beta1 にも残っており、TTY判定に任せず色を強制/抑止する手段は維持されています。
いま3.13系で書くときに避けるべき記法
結論を先に言うと、4.0.0.beta1 を本番のCIに入れるのはまだ早いです。ベータ版であり、rspec-rails 8.0.4 の依存も rspec-core >= 3.13.0, < 5.0.0 と幅は許容しているものの、Rails向けの動作確認が積み上がっているのは3.13系です。
やるべきなのは、3.13系のまま4.0で通る書き方に寄せておくことです。具体的には should 系を expect に置換し、ファイル先頭を RSpec.describe に統一し、it_should_behave_like を it_behaves_like に直します。そのうえで spec_helper.rb に4.0の既定値を先取りで書いておくと、移行時の差分が実質ゼロになります。
RSpec.configure do |config|
config.order = :random
config.mock_with(:rspec) { |mocks| mocks.verify_partial_doubles = true }
config.expect_with(:rspec) { |c| c.strict_predicate_matchers = true }
end
この設定を入れた直後は既存テストが赤くなるはずですが、それは4.0で必ず踏む地雷を前倒しで踏んでいるだけです。ベータ版が正式リリースされてから一括で直すより、平時に少しずつ潰す方が安全です。
よくある質問
RSpecとは何ですか?
Ruby向けのテストフレームワークです。describe・context・it という入れ子で「どんな状況で、どう振る舞うべきか」を記述し、expect とマッチャーで期待値を検証します。実体は rspec-core・rspec-expectations・rspec-mocks の3つをまとめたメタgemで、Rails向けには別途 rspec-rails が要ります。ブラウザ操作やカバレッジ計測はRSpecの守備範囲外で、Capybara や SimpleCov といった別gemの担当です。
Minitestでも describe や it は書けますか?
書けます。minitest/spec を読み込むと describe と it が使え、検証は _(1 + 1).must_equal 2 のように書きます。つまり「RSpec風の見た目」だけが目的ならMinitestで足ります。RSpecを選ぶ実質的な差分は、let・subject・shared_examples による前提の共有と、--bisect や --only-failures といった実行制御の作り込みです。この2つが要らない規模ならMinitestで十分で、既存プロジェクトはどちらかに揃えて混在させないでください。
RSpecのGitHubリポジトリはどこですか?
現在の開発は rspec/rspec というモノレポで行われています。以前の rspec/rspec-core・rspec/rspec-expectations・rspec/rspec-mocks の各リポジトリは、2024年11月30日を最後にアーカイブ済みです。issueやPRを出す場合、コードを読む場合は monorepo 側を見てください。rspec-rails だけは rspec/rspec-rails として独立したまま開発が続いています。
rspec-railsはどのRailsバージョンから使えますか?
Rails 7.2以上なら rspec-rails 8.0系、Rails 7.1以前なら 7.1.1 以前を指定します。RSpec本体側の依存は rspec-core >= 3.13.0, < 5.0.0 と幅が広く、式のうえでは4.0系も範囲内です。ただし 4.0.0.beta1 はプレリリース版で、Bundlerは gem 'rspec-core', '4.0.0.beta1' のように明示指定しない限り解決しません。bundle update で不意に4.0系が入ることはないので、そこは安心してください。
できます。RSpec.shared_examples '価格つきリソース' do |expected| のようにブロック引数を受け、呼び出し側で it_behaves_like '価格つきリソース', 300 と渡します。期待値だけが違う同型のテストをまとめるときに有効です。前提データそのものを共有したい場合は shared_examples ではなく shared_context を使い、include_context で取り込みます。前者はexample(it)を持ち込み、後者は let や before を持ち込む、と役割で覚えてください。