RSpecでテストを書いていると、「あれ、このテストコード、他の場所でも書いたな…」と感じることがありませんか?特に、似たような振る舞いをするクラスや、同じようなセットアップが必要なテストが増えてくると、コードの重複は避けられない問題になりがちです。
重複したコードは、修正が必要になった際に複数の箇所を変更する必要があり、バグの温床になったり、メンテナンスコストを増大させたりします。この問題を解決し、テストコードをDRY (Don’t Repeat Yourself) に保つための強力な武器が、RSpecに用意されているshared_examples
とshared_context
です。
この記事では、RSpecのテストコード共通化に不可欠なshared_examples
とshared_context
について、基本的な使い方から違い、実践的な使い分け、注意点まで、サンプルコードを交えながら「完全ガイド」として徹底解説します。
この記事を読むことで、あなたは以下のことができるようになります。
shared_examples
とshared_context
の基本的な使い方を理解する。- 両者の明確な違いと、適切な使い分け方を判断できるようになる。
- テストコードの重複を減らし、より保守性の高いコードを書くためのベストプラクティスを知る。
RSpecを使い始めたばかりの方から、テストコードのリファクタリングを考えている方まで、ぜひ参考にしてください。
shared_examplesとshared_contextとは?
まず、それぞれの基本的な役割を理解しましょう。
shared_examples
: 複数のテストグループ(describe
やcontext
ブロック)で**共通の振る舞い(一連のit
テスト例)**を共有するために使います。「異なる対象が同じように動作すること」をテストするのに適しています。shared_context
: 複数のテストグループで**共通のセットアップ(before
フック、let
変数、ヘルパーメソッドなど)**を共有するために使います。「同じ前提条件の下で異なるテスト」を実行するのに適しています。
この違いが、両者を使い分ける上での最も重要なポイントです。
shared_examples
: テストの「振る舞い」を共有する
同じようなテストケースが複数の場所に現れる場合にshared_examples
が役立ちます。
基本的な書き方と使い方
shared_examples
は以下のように定義します。
Ruby
# spec/support/shared_examples/some_behavior.rb (推奨される配置場所)
RSpec.shared_examples "何らかの共通の振る舞い" do
# ここに共通の `it` ブロックを記述する
# let や subject は呼び出し側で定義されることを期待する
it '○○であること' do
# expect(subject)....
end
it '△△を返すこと' do
# expect(subject.some_method)....
end
end
そして、この共通の振る舞いをテストしたい場所でit_behaves_like
を使って呼び出します。
Ruby
# spec/models/class_a_spec.rb
require 'rails_helper'
# require 'support/shared_examples/some_behavior' # 個別にrequireする場合
RSpec.describe ClassA do
subject { described_class.new(...) }
# 必要に応じて let で変数を定義
it_behaves_like "何らかの共通の振る舞い"
end
# spec/models/class_b_spec.rb
require 'rails_helper'
# require 'support/shared_examples/some_behavior'
RSpec.describe ClassB do
subject { described_class.new(...) }
# 必要に応じて let で変数を定義
it_behaves_like "何らかの共通の振る舞い"
end
it_behaves_like
の代わりにinclude_examples
を使うこともできますが、it_behaves_like
は独立したcontext
として展開されるため、テスト結果の出力が見やすいことが多いです。
(補足) shared_examples_for
というエイリアス(別名)もありますが、現在はshared_examples
の使用が一般的です。
引数を渡す
shared_examples
に引数を渡すことで、テストの振る舞いをより柔軟にカスタマイズできます。
Ruby
# spec/support/shared_examples/status_check.rb
RSpec.shared_examples "特定のステータスを持つ" do |expected_status|
it "ステータスが #{expected_status} であること" do
expect(subject.status).to eq(expected_status)
end
end
# spec/models/order_spec.rb
RSpec.describe Order do
context '作成直後の場合' do
subject { Order.create(...) }
it_behaves_like "特定のステータスを持つ", "pending"
end
context '発送済みの場合' do
subject { Order.create(..., shipped_at: Time.current) }
it_behaves_like "特定のステータスを持つ", "shipped"
end
end
let
やsubject
との連携
shared_examples
内で使われるlet
変数やsubject
は、呼び出し元のdescribe
やcontext
ブロックで定義されたものが使われます。これにより、異なる対象(subject
)に対して同じテスト(shared_examples
)を適用できます。
shared_context
: テストの「セットアップ」を共有する
テストを実行するための前提条件(データの準備、ログイン状態、ヘルパーメソッドなど)が複数のテストで共通している場合にshared_context
が役立ちます。
基本的な書き方と使い方
shared_context
は以下のように定義します。
Ruby
# spec/support/shared_contexts/login_user.rb (推奨される配置場所)
RSpec.shared_context "ログイン済みのユーザー" do
let(:current_user) { FactoryBot.create(:user) } # 例: FactoryBotを使用
before do
# ここにログイン処理のシミュレーションなどを記述
sign_in(current_user) # Devise のヘルパーメソッド sign_in を使う例
end
def common_helper_method
# テストで使える共通のヘルパーメソッド
"shared helper result"
end
end
そして、この共通のセットアップを使いたい場所でinclude_context
を使って呼び出します。
Ruby
# spec/controllers/posts_controller_spec.rb
require 'rails_helper'
# require 'support/shared_contexts/login_user' # 個別にrequireする場合
RSpec.describe PostsController, type: :controller do
describe 'ログインが必要なアクション' do
# 共通のセットアップを読み込む
include_context "ログイン済みのユーザー"
it 'GET #index は成功すること' do
get :index
expect(response).to be_successful
# ここでは current_user が利用可能
# common_helper_method も利用可能
end
it 'POST #create は投稿を作成できること' do
post :create, params: { post: { title: 'Test', body: '...' } }
# ... アサーション ...
end
end
describe 'ログインが不要なアクション' do
# ここでは include_context を呼び出さない
it 'GET #show は成功すること' do
# ...
end
end
end
include_context
を呼び出したスコープ内では、shared_context
内で定義されたlet
変数、before
フック、ヘルパーメソッドが利用可能になります。
shared_examples
vs. shared_context
: 違いと比較、使い分け
ここで、両者の違いを整理し、どちらを使うべきか判断する基準を明確にしましょう。
比較項目 | shared_examples | shared_context |
主な目的 | 振る舞い(テスト例)の共有 | セットアップ(前提条件)の共有 |
共有されるもの | it specify ブロック群 | let subject before ヘルパーメソッド等 |
呼び出し方法 | it_behaves_like include_examples | include_context |
ユースケース | ・異なる対象の同じ振る舞いをテスト ・インターフェースのテスト | ・共通のデータ準備 ・ログイン状態の再現 ・共通ヘルパー |
使い分けのヒント:
- 「この一連のテスト(
it
ブロック群)、他の場所でも全く同じように使いたいな」 ->shared_examples
- 「このテストの準備(
let
やbefore
)、他のテストでも必要だな」 ->shared_context
これらは組み合わせて使うことも可能です。例えば、shared_context
で共通のセットアップを行い、そのコンテキスト内でshared_examples
を使って共通の振る舞いをテストする、といった使い方もできます。
ベストプラクティスと注意点
shared_examples
とshared_context
は強力ですが、使い方を間違えると逆にテストを複雑にしてしまう可能性もあります。以下の点に注意しましょう。
1.ファイル構成:
- 共有するコードは
spec/support/shared_examples
やspec/support/shared_contexts
といった専用ディレクトリに置くのが一般的です。 - これらのファイルは
rails_helper.rb
(またはspec_helper.rb
) で一括してrequire
するか、必要なファイルで個別にrequire
します。 Ruby
# spec/rails_helper.rb or spec/spec_helper.rb
# Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f } # 一括ロード例
2.明確な命名:
- 共有ブロックには、その内容が明確にわかる名前(
"..."
の部分)を付けましょう。「何が」共有されているのかが一目でわかるようにすることが重要です。
3.スコープを小さく保つ:
- 一つの共有ブロックにあれもこれも詰め込みすぎず、関心事を分離しましょう。肥大化した共有ブロックは理解や再利用が困難になります。
4.依存関係を意識する:
shared_examples
が特定のlet
変数が定義されていることを暗黙的に期待している場合、その依存関係がわかりにくくなることがあります。必要に応じてドキュメントコメントを残したり、引数で明示的に渡したりすることを検討しましょう。
5.乱用しない:
- 共通化は目的ではなく手段です。無理に共通化しようとして、かえってテストコードが複雑で読みにくくなっては本末転倒です。可読性とDRYのバランスを常に意識しましょう。
まとめ
RSpecのshared_examples
とshared_context
は、テストコードの重複を減らし、保守性と可読性を向上させるための強力な機能です。
- 振る舞いを共有するなら
shared_examples
- セットアップを共有するなら
shared_context
この基本を理解し、今回紹介した使い方やベストプラクティスを参考に、あなたのテストコードをよりクリーンで効率的なものに改善していきましょう。
DRYなテストは、将来のコード変更に対する自信を与えてくれます。ぜひ、積極的にこれらの機能を活用してみてください。
より詳細な情報については、RSpecの公式ドキュメントなども参照することをお勧めします]。