Rails アプリケーションで RSpec を使ってテストを書く際の、実践的なベストプラクティスをまとめたドキュメントです。


目次

  1. 基本方針
  2. セットアップと推奨 Gem
  3. ディレクトリ構成とスペックの種類
  4. describe / context / it の書き方
  5. let / let! / subject / before の使い分け
  6. FactoryBot の使い方
  7. スペック種類別のプラクティス
  8. モック・スタブ
  9. 時間・外部API・非同期処理
  10. 共通化(shared_examples / shared_context / support)
  11. パフォーマンスと安定性
  12. Lint・カバレッジ・CI
  13. アンチパターン集
  14. チェックリスト

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
end

spec/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 SpecHTTP リクエスト単位の入出力、認証・認可、ステータスコード◎
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
end
  • before に 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
end

6. 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
end

7. スペック種類別のプラクティス

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 の既定。情報が多く実績がある
CupriteChrome 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
end

Rails 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
end
  • ActiveSupport::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"
end

shared_context:共通のセットアップ

RSpec.shared_context "with logged in admin" do
  let(:admin) { create(:user, :admin) }
  before { sign_in admin }
end

support ヘルパー

# 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
end
  • rspec --profile 10 で遅いテストを可視化し、定期的に改善する。
  • System Spec は必要最小限に。JS が不要なら rack_test を使う。
  • 大規模になったら並列実行(parallel_tests や Rails 標準の並列化、CI でのシャーディング)を導入する。
  • test-prof gem(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 Guestexample 単体で意図が読めない前提を 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 の指摘がないか

0件のページ