Rails アプリケーションで RSpec を使ってテストを書く際の、実践的なベストプラクティスをまとめたドキュメントです。
目次
- 基本方針
- セットアップと推奨 Gem
- ディレクトリ構成とスペックの種類
- describe / context / it の書き方
- let / let! / subject / before の使い分け
- FactoryBot の使い方
- スペック種類別のプラクティス
- モック・スタブ
- 時間・外部API・非同期処理
- 共通化(shared_examples / shared_context / support)
- パフォーマンスと安定性
- Lint・カバレッジ・CI
- アンチパターン集
- チェックリスト
1. 基本方針
- 振る舞いをテストする。実装をテストしない。 内部のメソッド呼び出し順ではなく、入力に対する出力・副作用を検証する。
- テストはドキュメントである。
--format documentationで出力したときに仕様書として読めることを目指す。 - 1つの example では1つの振る舞いを検証する。 複数の expect を書くこと自体は問題ないが、検証している「関心事」は1つにする。
- 読みやすさ > DRY。 テストコードは多少の重複を許容してでも、上から読んで理解できることを優先する。
- テストピラミッドを意識する。 遅く壊れやすい System Spec は主要導線に絞り、ロジックの網羅はモデル・サービスなどの下位層で行う。
/\ System Spec(主要なユーザー導線のみ)
/ \
/----\ Request Spec(API・エンドポイント単位)
/ \
/--------\ Model / Service / Job など(ロジックの網羅)
2. セットアップと推奨 Gem
Gemfile
group :development, :test do
gem "rspec-rails"
gem "factory_bot_rails"
gem "faker"
end
group :test do
gem "shoulda-matchers" # バリデーション・関連のワンライナー検証
gem "capybara" # System Spec の DSL
gem "capybara-playwright-driver" # ブラウザドライバー(selenium-webdriver / cuprite も可)
gem "webmock" # 外部HTTPの遮断・スタブ
gem "vcr" # 外部APIレスポンスの記録・再生(必要に応じて)
gem "simplecov", require: false
end
group :development do
gem "rubocop-rspec", require: false
gem "rubocop-factory_bot", require: false
gem "rubocop-rspec_rails", require: false
end初期化
bin/rails generate rspec:install.rspec
--require spec_helper
--format documentation
--color
spec/rails_helper.rb の主な設定
require "spec_helper"
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
abort("The Rails environment is running in production mode!") if Rails.env.production?
require "rspec/rails"
# spec/support 配下を読み込む
Rails.root.glob("spec/support/**/*.rb").sort.each { |f| require f }
# 保留中のマイグレーションがあれば落とす
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
abort e.to_s.strip
end
RSpec.configure do |config|
config.fixture_paths = [Rails.root.join("spec/fixtures")]
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
config.include FactoryBot::Syntax::Methods
config.include ActiveSupport::Testing::TimeHelpers
end
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
endspec/spec_helper.rb で有効にしておきたい設定
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
# 存在しないメソッドのスタブを禁止(verifying doubles)
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.filter_run_when_matching :focus # fit / fdescribe で絞り込み
config.example_status_persistence_file_path = "spec/examples.txt" # --only-failures 用
config.disable_monkey_patching! # `RSpec.describe` を強制
config.order = :random # テスト順序依存の検出
Kernel.srand config.seed
config.profile_examples = 10 if ENV["PROFILE"] # 遅いテストの可視化
end
config.order = :randomは必ず有効にする。順序依存のテストは早期に発見するほど修正コストが小さい。
3. ディレクトリ構成とスペックの種類
spec/
├── factories/ # FactoryBot 定義
├── models/ # Model Spec
├── requests/ # Request Spec(コントローラのテストはこちら)
├── system/ # System Spec(E2E)
├── services/ # Service Object など PORO
├── jobs/ # Job Spec
├── mailers/ # Mailer Spec
├── policies/ # 認可(Pundit 等)
├── forms/ queries/ ... # アプリの設計に合わせる
├── support/ # 共通ヘルパー・shared_context・カスタムマッチャ
├── rails_helper.rb
└── spec_helper.rb
| 種類 | 用途 | 推奨度 |
|---|---|---|
| Model Spec | バリデーション、スコープ、ドメインロジック | ◎ |
| Request Spec | HTTP リクエスト単位の入出力、認証・認可、ステータスコード | ◎ |
| System Spec | ブラウザ操作を伴うユーザー導線 | ○(主要導線に絞る) |
| Service / PORO Spec | ビジネスロジック | ◎ |
| Job / Mailer Spec | 非同期処理、メール内容 | ○ |
| Controller Spec | — | △(Rails 5 以降は Request Spec を推奨) |
| View / Routing / Helper Spec | — | △(必要な場合のみ) |
| Feature Spec | — | ×(System Spec に置き換える) |
spec/のディレクトリ構成はapp/と対応させる。app/services/orders/create.rb→spec/services/orders/create_spec.rb- ファイル名は必ず
_spec.rbで終える。
4. describe / context / it の書き方
基本ルール
describe:テスト対象(クラス、メソッド)を書く- インスタンスメソッドは
#method、クラスメソッドは.method
- インスタンスメソッドは
context:条件・状態を書く。「〜の場合」「when / with / without」で始めるit:期待する振る舞いを書く。shouldは使わず、三人称現在形で書く
RSpec.describe User, type: :model do
describe "#full_name" do
context "when middle name is present" do
it "returns first, middle and last name" do
user = build(:user, first_name: "Taro", middle_name: "J", last_name: "Yamada")
expect(user.full_name).to eq "Taro J Yamada"
end
end
context "when middle name is blank" do
it "returns first and last name" do
user = build(:user, first_name: "Taro", middle_name: nil, last_name: "Yamada")
expect(user.full_name).to eq "Taro Yamada"
end
end
end
describe ".active" do
it "returns only active users" do
active = create(:user, :active)
create(:user, :inactive)
expect(User.active).to contain_exactly(active)
end
end
end日本語で書く場合
チームで合意していれば日本語の記述でも問題ない。統一されていることが重要。
describe "#full_name" do
context "ミドルネームがある場合" do
it "ミドルネームを含むフルネームを返す" do
# ...
end
end
endポイント
- 正常系・異常系・境界値を context で明示的に分ける。 対になる context(ある場合 / ない場合)を揃えると抜け漏れに気づきやすい。
- ネストは深くても3〜4階層程度まで。 それ以上深くなるなら、テスト対象の責務が大きすぎるサイン。
itの説明文が長くなりすぎる場合は context に切り出す。
5. let / let! / subject / before の使い分け
let(遅延評価)を基本にする
let(:user) { create(:user) }- 参照されたときに初めて評価され、同じ example 内ではメモ化される。
- インスタンス変数(
@user)よりletを使う。typo がnilではなく NameError になるため安全。
let!(即時評価)は必要なときだけ
# 「参照されないが、DB に存在している必要がある」データに使う
let!(:archived_post) { create(:post, :archived) }
it "does not include archived posts" do
expect(Post.published).not_to include(archived_post)
end- 乱用すると不要なレコードが毎回作られ、テストが遅くなり意図も読みづらくなる。
- 多くの場合、example 内で明示的に作るほうが読みやすい。
subject はテスト対象の呼び出しに使う
describe "#call" do
subject(:result) { described_class.new(order).call }
let(:order) { create(:order, :pending) }
it "marks the order as paid" do
expect { result }.to change { order.reload.status }.from("pending").to("paid")
end
end- 名前付き subject(
subject(:result))を使う。 無名のsubjectをis_expected以外で参照すると読みにくい。 is_expectedのワンライナーは、shoulda-matchers など自明な場合に限定する。
it { is_expected.to validate_presence_of(:email) }before はセットアップ・前提条件に使う
before do
sign_in user
endbeforeに expect を書かない。- データ作成は
beforeに@var = ...で書くよりletを使う。
「Mystery Guest」を避ける
テストに必要な前提が遠く離れた場所(外側の let、before、shared_context)にあると、example 単体を読んだときに何をテストしているのか分からなくなる。そのテストの結果に影響する値は、その example の近くに書く。
# ❌ 何が 1000 円の根拠なのか分からない
it "returns total price" do
expect(order.total_price).to eq 1000
end
# ✅ 前提が見える
it "returns the sum of line item prices" do
order = create(:order)
create(:line_item, order:, price: 300)
create(:line_item, order:, price: 700)
expect(order.total_price).to eq 1000
end6. FactoryBot の使い方
ファクトリは「最小限で有効なレコード」にする
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { "Test User" }
password { "password" }
# バリエーションは trait で表現する
trait :admin do
role { :admin }
end
trait :with_posts do
transient do
posts_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:post, evaluator.posts_count, user:)
end
end
end
end- デフォルトでは必須属性のみを定義する。オプショナルな属性や関連は
traitに切り出す。 - 一意制約のある属性は
sequenceを使う。 Fakerはランダム性により再現困難な失敗を招くことがある。値が検証に関係する箇所では固定値を使う。- テストで検証する値は、ファクトリのデフォルト値に依存せず、テスト内で明示的に指定する。
build / build_stubbed / create を使い分ける
| メソッド | DB保存 | 速度 | 用途 |
|---|---|---|---|
build_stubbed | しない(IDあり、永続化済みのふり) | 最速 | DB を必要としないロジックのテスト |
build | しない | 速い | バリデーション、保存前の状態のテスト |
create | する | 遅い | スコープ、クエリ、関連の取得など DB が必要なテスト |
attributes_for | — | — | Request Spec のパラメータ作成 |
「DB に保存する必要があるか」を毎回考え、不要なら build / build_stubbed を使う。 テスト速度に最も効く。
関連の書き方
factory :post do
user # association :user と同じ
title { "Title" }
end- 関連のデフォルト生成は、テストが大量のレコードを連鎖的に作る原因になる。
FactoryBot.use_parent_strategy = true(デフォルト)を維持し、build時に関連もbuildされるようにする。 - 深い関連チェーンを持つファクトリは定期的に見直す。
ファクトリの妥当性をチェックする
# spec/factories_spec.rb
RSpec.describe "FactoryBot" do
it "has valid factories" do
FactoryBot.lint traits: true
end
end7. スペック種類別のプラクティス
7.1 Model Spec
RSpec.describe User, type: :model do
describe "validations" do
subject { build(:user) }
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it { is_expected.to validate_length_of(:name).is_at_most(50) }
end
describe "associations" do
it { is_expected.to have_many(:posts).dependent(:destroy) }
it { is_expected.to belong_to(:organization).optional }
end
describe "#deactivate!" do
let(:user) { create(:user, :active) }
it "sets deactivated_at" do
freeze_time do
user.deactivate!
expect(user.deactivated_at).to eq Time.current
end
end
end
end- バリデーション・関連は shoulda-matchers で簡潔に書く。
- Rails 自体の機能(
has_manyが動くこと等)はテストしない。 自分で書いたロジック・設定をテストする。 - カスタムバリデーション、スコープ、コールバック、ドメインメソッドを重点的にテストする。
- コールバックが複雑な場合は、そもそも Service Object へ切り出すことを検討する。
7.2 Request Spec
Controller Spec ではなく Request Spec を使う。ルーティング・ミドルウェア・コントローラを通して検証でき、実際の挙動に近い。
RSpec.describe "Posts API", type: :request do
let(:user) { create(:user) }
let(:headers) { { "Authorization" => "Bearer #{token_for(user)}" } }
describe "POST /api/posts" do
let(:params) { { post: { title: "Hello", body: "World" } } }
context "with valid params" do
it "creates a post" do
expect {
post "/api/posts", params:, headers:, as: :json
}.to change(Post, :count).by(1)
end
it "returns 201 with the created post" do
post "/api/posts", params:, headers:, as: :json
expect(response).to have_http_status(:created)
expect(response.parsed_body).to include("title" => "Hello")
end
end
context "with invalid params" do
let(:params) { { post: { title: "" } } }
it "returns 422" do
post "/api/posts", params:, headers:, as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
context "when unauthenticated" do
it "returns 401" do
post "/api/posts", params:, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
end- ステータスコードはシンボル(
:created,:not_found)で書く。 - 認証・認可の境界(未ログイン、他人のリソース、権限不足)は必ずテストする。
- JSON は
response.parsed_bodyで検証する。レスポンス構造が重要なら JSON Schema(json_matchers等)やmatch+hash_includingを使う。 - Devise を使う場合は
Devise::Test::IntegrationHelpersを include してsign_inを使う。
7.3 System Spec
Capybara とドライバーの関係
System Spec は「Capybara(テストを書く DSL)」と「ドライバー(実際にブラウザを操作する層)」の2層構造になっている。テストコードは Capybara の DSL で書き、ドライバーは設定で差し替える。
| ドライバー | 特徴 |
|---|---|
rack_test | ブラウザを起動しない。最速だが JS は動かない |
Playwright(capybara-playwright-driver) | 自動待機が強く安定しやすい。Chromium / Firefox / WebKit に対応。Node.js の Playwright が必要。Rails 7.1 以降で driven_by :playwright が使える。新規プロジェクトでの第一候補 |
Selenium(selenium-webdriver) | Rails の既定。情報が多く実績がある |
| Cuprite | Chrome DevTools Protocol で直接操作。Node.js 不要で軽量 |
Playwright のセットアップ
bundle add capybara-playwright-driver --group test
npm install --save-dev playwright # gem が対応するバージョンに合わせる
npx playwright install chromium# spec/support/system_spec.rb
RSpec.configure do |config|
config.before(:each, type: :system) do
driven_by :rack_test
end
config.before(:each, type: :system, js: true) do
driven_by :playwright, options: { browser_type: :chromium, headless: true }
end
endRails 7.1 以降では
driven_by :playwrightを呼ぶと Rails が:playwrightドライバーを登録し直すため、自分でCapybara.register_driver(:playwright)したオプションは上書きされる。オプションはdriven_byのoptions:で渡すか、別名でドライバーを登録する。gem(playwright-ruby-client)と npm の
playwrightのバージョンがずれると起動に失敗することがある。CI でも同じバージョンに固定する。
テストの例
RSpec.describe "User signs up", type: :system do
it "allows a visitor to sign up" do
visit new_user_registration_path
fill_in "メールアドレス", with: "new@example.com"
fill_in "パスワード", with: "password"
click_button "登録する"
expect(page).to have_content "登録が完了しました"
end
context "with JavaScript", js: true do
it "shows validation errors without page reload" do
# Playwright で実行される
end
end
end- デフォルトは
rack_test、JS が必要なテストだけヘッドレスブラウザ(Playwright 等)を使う。 - ドライバーを変えても、Capybara の DSL で書いたテストはほぼそのまま動く。Playwright 固有の API(ネットワークの監視、トレースなど)が必要な場合は
page.driver.with_playwright_page { |pw_page| ... }で触れられるが、使う範囲は最小限にする。 sleepは使わない。Capybara のhave_content/have_selectorなどは自動で待機する。expect(page.has_content?(...)).to be trueではなくexpect(page).to have_content(...)を使う(待機機構が働くため)。- セレクタは CSS クラスより、ラベル・ボタンのテキストや
data-testidなど変更に強いものを使う。 - 「ユーザーの目に見える結果」を検証する。DB を直接覗くのは最小限にする。
- ログインなど前提操作は UI を毎回経由せず、ヘルパー(
sign_in等)で済ませる。
7.4 Service Object / PORO Spec
RSpec.describe Orders::Checkout do
describe "#call" do
subject(:result) { described_class.new(order:, payment_gateway:).call }
let(:order) { create(:order, :with_items) }
let(:payment_gateway) { instance_double(PaymentGateway, charge: charge_result) }
context "when payment succeeds" do
let(:charge_result) { PaymentGateway::Result.new(success: true) }
it { is_expected.to be_success }
it "marks the order as paid" do
expect { result }.to change { order.reload.status }.to("paid")
end
end
context "when payment fails" do
let(:charge_result) { PaymentGateway::Result.new(success: false, error: "declined") }
it { is_expected.to be_failure }
it "does not change the order status" do
expect { result }.not_to change { order.reload.status }
end
end
end
end- 依存を注入できる設計にしておくと、テストで差し替えやすい。
- 戻り値と副作用(DB 変更、ジョブ投入、メール送信)の両方を検証する。
7.5 Job Spec
RSpec.describe ReportGenerationJob, type: :job do
include ActiveJob::TestHelper
it "enqueues the job" do
expect {
described_class.perform_later(report.id)
}.to have_enqueued_job(described_class).with(report.id).on_queue("default")
end
it "generates the report" do
report = create(:report, :pending)
described_class.perform_now(report.id)
expect(report.reload).to be_completed
end
end- 「キューに積まれること」と「実行したときの処理内容」は分けてテストする。
- ジョブの引数には ActiveRecord オブジェクトではなく ID を渡す設計にしておくと扱いやすい。
7.6 Mailer Spec
RSpec.describe UserMailer, type: :mailer do
describe "#welcome" do
let(:user) { build_stubbed(:user, email: "taro@example.com", name: "Taro") }
let(:mail) { described_class.welcome(user) }
it "renders the headers" do
expect(mail.to).to eq ["taro@example.com"]
expect(mail.subject).to eq "ようこそ"
end
it "includes the user name in the body" do
expect(mail.body.encoded).to include "Taro"
end
end
end- 呼び出し側では
have_enqueued_mail(UserMailer, :welcome)で送信のトリガーを検証する。
8. モック・スタブ
基本方針
- 自分が管理していない境界(外部API、決済、時刻、乱数)をモックする。
- 自分のアプリ内部のオブジェクトは、できるだけ本物を使う。 過剰なモックは「テストは通るが本番で動かない」状態を生む。
- テスト対象自身のメソッドはスタブしない(
allow(subject).to receive(...)は設計見直しのサイン)。
Verifying Doubles を使う
# ❌ 存在しないメソッドでもスタブできてしまう
gateway = double("PaymentGateway", charge: true)
# ✅ PaymentGateway に charge が存在しない・引数が合わない場合にエラー
gateway = instance_double(PaymentGateway, charge: true)instance_double/class_double/object_doubleを使う。verify_partial_doubles = trueを有効にしておく(§2 参照)。
allow と expect の使い分け
# スタブ(戻り値を決めたいだけ)
allow(gateway).to receive(:charge).and_return(result)
# メッセージ送信そのものが検証対象
expect(notifier).to receive(:notify).with(user)
# 呼び出し後に検証したい場合(Arrange-Act-Assert の順序を保てる)
notifier = instance_double(Notifier, notify: nil)
service.call
expect(notifier).to have_received(:notify).with(user)have_received(spy スタイル)を推奨。 Arrange → Act → Assert の順で読めるため。
allow_any_instance_of は避ける
# ❌ 影響範囲が広く、設計の問題を隠す
allow_any_instance_of(User).to receive(:premium?).and_return(true)
# ✅ 状態を作る or 依存を注入する
user = build(:user, :premium)9. 時間・外部API・非同期処理
時間
it "expires after 30 days" do
token = create(:token)
travel_to 31.days.from_now do
expect(token).to be_expired
end
end
it "records the current time" do
freeze_time do
record.touch_processed!
expect(record.processed_at).to eq Time.current
end
endActiveSupport::Testing::TimeHelpers(travel_to,freeze_time)を使う。Timecop は不要。Time.now/Date.todayではなくTime.current/Date.currentを使う(タイムゾーン問題の回避)。- 月末・年末・うるう年・タイムゾーン境界など、日付に依存するロジックは境界値で固定してテストする。
外部 HTTP
# spec/support/webmock.rb
require "webmock/rspec"
WebMock.disable_net_connect!(allow_localhost: true)before do
stub_request(:get, "https://api.example.com/users/1")
.to_return(status: 200, body: { name: "Taro" }.to_json,
headers: { "Content-Type" => "application/json" })
end- テスト中の外部通信は全面的に禁止する。 意図しない通信をエラーで検知できる。
- 外部APIのクライアントはラッパークラスに閉じ込め、アプリ側のテストではそのラッパーを
instance_doubleで差し替えるとシンプルになる。 - レスポンスが複雑な場合は VCR でカセットを記録する。カセットの機密情報は
filter_sensitive_dataでマスクする。 - タイムアウト・5xx・不正なレスポンスなどの異常系もテストする。
非同期処理
# 呼び出し側:ジョブが積まれることだけを検証
expect { service.call }.to have_enqueued_job(NotifyJob).with(user.id)
# 実行までまとめて検証したい場合
perform_enqueued_jobs do
service.call
end- テスト環境のキューアダプタは
:testにしておく。
10. 共通化(shared_examples / shared_context / support)
shared_examples:共通の振る舞い
# spec/support/shared_examples/requires_authentication.rb
RSpec.shared_examples "requires authentication" do
context "when unauthenticated" do
let(:headers) { {} }
it "returns 401" do
request
expect(response).to have_http_status(:unauthorized)
end
end
end
# 使用側
describe "GET /api/posts" do
subject(:request) { get "/api/posts", headers: }
it_behaves_like "requires authentication"
endshared_context:共通のセットアップ
RSpec.shared_context "with logged in admin" do
let(:admin) { create(:user, :admin) }
before { sign_in admin }
endsupport ヘルパー
# spec/support/request_helpers.rb
module RequestHelpers
def json
response.parsed_body
end
end
RSpec.configure do |config|
config.include RequestHelpers, type: :request
end注意点
- 共通化しすぎない。 shared_examples が多重にネストしたり、暗黙の
letに依存したりすると、テストが読めなくなる。 - shared_examples が依存する
let/subjectは、名前と役割をコメントで明記する。 - 「2〜3箇所で同じものが出てきたら」くらいを共通化の目安にする。
11. パフォーマンスと安定性
速くする
createを減らし、build/build_stubbedを使う(最も効果が大きい)。let!や外側のbeforeで不要なデータを作っていないか見直す。aggregate_failuresで、同じセットアップの重い example をまとめる。
it "returns the expected response", :aggregate_failures do
get "/api/posts/#{post.id}"
expect(response).to have_http_status(:ok)
expect(json["title"]).to eq post.title
expect(json["author"]["name"]).to eq post.user.name
endrspec --profile 10で遅いテストを可視化し、定期的に改善する。- System Spec は必要最小限に。JS が不要なら
rack_testを使う。 - 大規模になったら並列実行(
parallel_testsや Rails 標準の並列化、CI でのシャーディング)を導入する。 test-profgem(let_it_be,before_all, FactoryProf など)でボトルネックを特定・改善する。
安定させる(Flaky Test 対策)
config.order = :randomを有効にし、順序依存を検出する。失敗時は--seedで再現する。- テスト間で状態を共有しない(クラス変数、グローバル設定、キャッシュ、
ENVの書き換えは必ず元に戻す)。 - ID・並び順に依存しない。順序が不定なら
contain_exactly/match_arrayを使い、順序が仕様なら明示的にorderする。 - 時間依存は
freeze_time/travel_toで固定する。 - Rails.cache を使うテストでは、各テスト前にクリアする。
- System Spec で
sleepを使わない。Capybara の待機機構に任せる。 - Flaky なテストを
retryで隠さない。原因を特定して修正する。
12. Lint・カバレッジ・CI
rubocop-rspec
# .rubocop.yml
plugins:
- rubocop-rspec
- rubocop-rspec_rails
- rubocop-factory_bot
RSpec/ExampleLength:
Max: 15
RSpec/MultipleExpectations:
Max: 5
RSpec/NestedGroups:
Max: 4- チームの規約をコードで強制し、レビューでの指摘コストを減らす。
- デフォルト設定が厳しすぎる Cop は、チームで合意して調整する。
SimpleCov
# spec/spec_helper.rb の先頭
require "simplecov"
SimpleCov.start "rails" do
enable_coverage :branch
add_filter "/spec/"
end- カバレッジは「テストされていない箇所を見つける道具」であり、目標値そのものではない。 100% を追うより、重要なロジックの分岐が網羅されているかを見る。
- ブランチカバレッジも有効にする。
CI
- PR ごとに全テストを実行し、失敗したらマージできないようにする。
fit/fdescribeの消し忘れを防ぐため、CI では--fail-if-no-examplesや rubocop のRSpec/Focusで検知する。- 失敗時のスクリーンショット(System Spec)やログをアーティファクトとして保存する。
13. アンチパターン集
| アンチパターン | 問題 | 改善策 |
|---|---|---|
実装詳細のテスト(private メソッドを send で呼ぶ等) | リファクタリングでテストが壊れる | public インターフェース経由で検証する |
| 過剰なモック | 本番で動かないのにテストは通る | 境界だけモックし、内部は本物を使う |
allow_any_instance_of | 影響範囲が広く設計問題を隠す | 依存注入・状態の準備で対応 |
| Mystery Guest | example 単体で意図が読めない | 前提を example の近くに書く |
巨大な let! / before | 遅い・不要なデータ・意図不明 | 必要な example で必要なデータだけ作る |
| 深いネスト(5階層以上) | 読めない | context の整理、テスト対象の分割 |
it "works" のような曖昧な説明 | 失敗時に何が壊れたか分からない | 期待する振る舞いを具体的に書く |
sleep による待機 | 遅く不安定 | Capybara の待機マッチャを使う |
| Controller Spec / Feature Spec の新規作成 | 非推奨 | Request Spec / System Spec を使う |
| 外部APIに実際に通信 | 遅い・不安定・課金 | WebMock で遮断、スタブ化 |
| Rails の機能そのもののテスト | 価値が低い | 自分で書いたロジックに集中 |
| Flaky Test の放置・retry で隠蔽 | テストへの信頼が失われる | 原因を特定して修正 |
| コピペで量産された似たテスト | 保守が大変 | 適度に shared_examples 化(やりすぎ注意) |
14. チェックリスト
テストを書いた後・レビュー時に確認する項目です。
-
describe/context/itが仕様書として読めるか - 正常系だけでなく、異常系・境界値・権限のケースがあるか
- 1つの example が1つの振る舞いに集中しているか
- example 単体を読んで前提と期待値が分かるか(Mystery Guest がないか)
- 不要な
create/let!がないか(build/build_stubbedで済まないか) - モックは外部境界に限定され、verifying double を使っているか
- 時刻・外部通信・乱数など非決定的な要素を固定しているか
- ランダム順で実行しても通るか
- Controller Spec ではなく Request Spec、Feature Spec ではなく System Spec を使っているか
-
fit/fdescribe/binding.irbなどが残っていないか - rubocop-rspec の指摘がないか