rspec-best-practices について
rspec-best-practicesは、RSpecテストの作成、レビュー、整理を支援するスキルです。テスト構成、ファクトリー、モック、決定性、TDDの手順を示します。
主な機能
- 01テスト種別と構成の指針
- 02振る舞いを重視したアサーション
- 03最小限のファクトリーとモック
- 04決定的なテストの指針
- 05段階的なTDD手順
- 06出力規則と再利用可能なテンプレート
活用シーン
- 01Railsの振る舞いに対するRSpecテスト作成
- 02保守性を意識したスペックのレビュー
- 03不安定なテストや時刻依存テストの削減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ midudev/ autoskills --skill rspec-best-practicesインストール後、Claude Code で/rspec-best-practicesを入力して読み込まれたか確認します。
同梱ファイルassets/spec_templates.mdEXAMPLES.md
詳細
RSpec のベストプラクティス
RSpec テストの作成、レビュー、整理を行う場合は、このスキルを使用してください。
基本原則: 実装への依存よりも、振る舞いへの確信を優先します。優れたスペックは読みやすく、決定的で、保守コストが低いものです。
クイックリファレンス
| 項目 | ルール |
|---|---|
| スペックの種類 | Model:ドメインロジック、Request:HTTP エンドポイント(controller より優先)、Job:バックグラウンド処理、Service/PORO:Rails ヘルパーなし、System:重要な E2E のみ(低速) |
| アサーション | 実装ではなく振る舞いをテストします |
| ファクトリー | 最小限にします — 必要な属性のみを指定し、省略可能な状態にはトレイトを使います。create より build/build_stubbed を優先します |
| モック | 内部コードではなく、外部との境界をスタブします |
| 独立性 | 各 example は独立させ、共有の可変状態を持たせません |
| 命名 | クラス/メソッドには describe、シナリオには context を使います |
| Service spec | 必須: 主な呼び出しには describe '.call' と subject(:result) を使います |
let と let! | 既定では let を使います。オブジェクトを example の実行前に存在させる必要がある場合に限り let! を使います |
| 外部サービスのモック | allow(ServiceClass).to receive(:method) を使います — instance_double は使いません。instance_double は注入された協力オブジェクトに限って使います |
| Example の名前 | 現在形にします:it 'returns the user'。it 'should ...' は使いません。「and」という語を絶対に含めないでください — example を分割します |
aggregate_failures | 1 つの example 内で複数の関連項目を検証する場合に使います |
TDD ワークフロー
RSpec を使って新しい振る舞いを開発する場合は、次の手順に従ってください。
- 失敗するスペックを書く — 意図した振る舞いを検証できる、最小のスペック種別を選びます(model > service > request > system)。
- 実行して失敗メッセージを確認する — エラーはセットアップの問題ではなく、コードが不足していることを示す必要があります。
- スペックを通すために必要最小限のコードを実装する。
- リファクタリングする — テストスイートを成功状態に保ちながら、重複と命名を整理します。
- 検証する — コミットする前に、関連するスペックファイル全体を実行し、続けてテストスイート全体を実行します。
Rails の変更で最初に書くべき失敗スペックを選ぶ
| 変更の種類 | 最初に書くもの |
|---|---|
| 純粋なドメインロジック | Model または PORO の service spec |
| HTTP エンドポイントの振る舞い | Request spec |
| バックグラウンド処理 | Job spec |
| レイヤーをまたぐユーザージャーニー | System spec(控えめに使用) |
ファクトリーの設計
ファクトリーは最小限にします。ビジネスロジックにファクトリーのデフォルト値を決して頼らないでください — 明示的に設定するか、トレイトを使います。build/build_stubbed で十分な場合は create を避けます。
Service spec(基本パターン)
RSpec.describe Invoices::MarkOverdue do
describe '.call' do
subject(:result) { described_class.call(invoice: invoice) }
context 'when the invoice is overdue and unpaid' do
let(:invoice) { create(:invoice, due_date: 2.days.ago, paid_at: nil) }
it 'marks the invoice overdue' do
expect { result }.to change { invoice.reload.overdue? }.from(false).to(true)
end
end
context 'when the invoice is already paid' do
let(:invoice) { create(:invoice, due_date: 2.days.ago, paid_at: 1.day.ago) }
it 'does not change the invoice' do
expect { result }.not_to change { invoice.reload.updated_at }
end
end
end
end
→ 詳細な例:EXAMPLES.md | コピーして使えるテンプレート:assets/spec_templates.md
Shared Examples
example ごとの let の上書きなしに、同じ振る舞いの契約が複数の対象に適用される場合に限り使用します。各 context で異なるセットアップが必要になる場合は避けてください — それは抽象化が適切でない兆候です。→ EXAMPLES.md の例
1 つの example に 1 つの振る舞い — Example の名前に「and」を絶対に含めない
it/specify の説明に 「and」 という語が含まれている場合、その example は 2 つの振る舞いを検証しています。分割してください。example ごとに 1 つの振る舞いにします。これは model、request、service、job、mailer、system のすべてのスペック種別に適用されます。
# BAD — two assertions; if the first fails, the second never runs
it 'returns 201 and creates the record' do; end
it 'saves the order and sends the confirmation email' do; end
it 'updates the user and logs the change' do; end
# GOOD — one observable outcome per example
it 'returns 201' do; end
it 'creates the record' do; end
it 'saves the order' do; end
it 'sends the confirmation email' do; end
スペックを確定する前のセルフチェック: すべての it '...'/it "..."/specify '...' 文字列を検索し、「and」という語(大文字と小文字を区別せず、単語境界で判定)がないか確認してください。該当箇所はすべて分割します。「returns nil and does not raise」のような「利便性」のための example であっても例外はありません。
出力スタイル
RSpec のスペックの作成またはレビューを求められた場合、出力は以下の各ルールを必ず満たしてください。各ルールは個別に評価され、違反が1つでもあるとチェック全体に失敗します。
- スペックファイルのパスはソースを反映します:
app/foo/bar.rb→spec/foo/bar_spec.rb。 - **
# frozen_string_literal: true**を各スペックファイルの1行目に記述します。 - **
RSpec.describe**には完全な定数パス(RSpec.describe Module::Class do)を使用し、文字列は使用しません。 - テスト対象の各メソッドには**
describe '#method'/describe '.class_method'**を使用します。 - シナリオの違いには**
context 'when ...'/context 'with ...'**を使用します。メソッドをまとめるためにcontextを使用してはいけません。 - テストデータには
letを使用し、let!はテスト対象のアクションより前にオブジェクトが存在している必要がある場合にのみ使用します。 - プロジェクトがすでに
test-profに依存している場合を除き、let_it_beは使用しません(まずGemfile.lockを確認してください)。 - すべての例の説明文で「and」を使用してはいけません。出現するたびに分割してください(上のセクションを参照)。最も見落とされやすいルールです。スペックを返す前に、明示的に確認してください。
.callを呼び出すサービス/POROのスペックでは**subject(:result) { ... }**を使用します。- 時間に依存するアサーションには**
travel_to/freeze_time**を使用します。過去のTime.nowを設定したり、Time.currentを直接スタブしたりしてはいけません。 - 外部との境界はクラスメソッドレベルでモックします(
allow(SomeClient).to receive(:method))。ActiveRecordのファインダーは決してモックしません。
不安定なテストと決定的なアサーション
| 原因 | 修正方法 |
|---|---|
| 時間依存のロジック | freeze_time / travel_toを使用します。手軽な方法として過去の日付を設定してはいけません |
| 状態の漏れ | 各例で独自の状態をセットアップします。before(:all)は避けてください |
| 非同期ジョブ | queue_adapter = :test + have_enqueued_jobを使用します。副作用を命令的にアサートしてはいけません |
| 外部HTTP | WebMock / VCRを使用します。CIで実際のネットワーク通信を許可してはいけません |
| DB状態の混入 | トランザクションフィクスチャまたはDatabaseCleanerを使用します。コンテキスト間でlet!を共有してはいけません |
| 競合状態 | Capybaraの明示的な待機を使用します。sleepは避けてください |
| 精度の低いアサーション | 最終状態ではなくchange.from().to()を使用します。be_truthy / be_falseyではなく正確な値を使い、updated_atをアサートしてはいけません |