Rails RSpecテスト入門
信頼性の高いコードを書く
RSpecを使ったRailsアプリのテスト方法を解説。テストの書き方、Factory Bot、モデルスペック、コントローラスペックまで学べます。
こんな人向けの記事です
- Railsアプリのテストを書きたい
- RSpecの基本的な使い方を学びたい
- Factory Botでテストデータを管理したい
Step 1Railsにおけるテストの種類
Railsアプリケーションの品質を保つためには、さまざまなレベルのテストを組み合わせることが重要です。テストは大きく3つの種類に分けられます。
| テストの種類 | 対象 | 速度 | 目的 |
|---|---|---|---|
| ユニットテスト | モデル、バリデーション、メソッド単体 | 高速 | 個々のロジックが正しく動くか検証 |
| 結合テスト | コントローラー、APIエンドポイント | 中程度 | 複数のコンポーネントが連携して動くか検証 |
| システムテスト | ブラウザ操作、画面遷移 | 低速 | ユーザー視点で正しく動くか検証 |
テストピラミッドの考え方では、ユニットテストを多く、結合テストを中程度、システムテストを少なく書くのが理想です。ユニットテストは高速で安定しているため、最も費用対効果が高いテストです。
RSpecでは、それぞれの種類に対応するスペックファイルを以下のディレクトリに配置します。
spec/
├── models/ # ユニットテスト(モデルスペック)
│ └── user_spec.rb
├── requests/ # 結合テスト(リクエストスペック)
│ └── users_spec.rb
├── system/ # システムテスト
│ └── login_spec.rb
├── factories/ # Factory Bot のファクトリ定義
│ └── users.rb
├── rails_helper.rb # Rails固有の設定
└── spec_helper.rb # RSpec全体の設定
RailsのデフォルトテストフレームワークはMinitestですが、業界ではRSpecがより広く使われています。可読性の高いDSL(ドメイン固有言語)で、テストを自然言語に近い形で記述できるのが特徴です。
Step 2RSpecの導入と基本設定
RSpecをRailsプロジェクトに導入するには、Gemfileに必要なgemを追加します。
group :development, :test do
gem "rspec-rails", "~> 7.0"
gem "factory_bot_rails"
gem "faker"
end
group :test do
gem "shoulda-matchers"
gem "capybara"
gem "selenium-webdriver"
end
gemを追加したら、インストールと初期設定を行います。
# gemのインストール
$ bundle install
# RSpecの初期ファイル生成
$ rails generate rspec:install
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
生成された.rspecファイルにオプションを追加します。
--require spec_helper
--format documentation
--color
--format documentationを指定すると、テスト実行時にdescribeやitの説明文が階層的に表示され、テスト結果が読みやすくなります。
続いてrails_helper.rbにFactory BotとShoulda Matchersの設定を追加します。
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"
# Factory Botのメソッドを直接使えるようにする
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
# テストごとにDBをクリーンな状態にする
config.use_transactional_fixtures = true
# スペックのタイプを自動推論する
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
end
# Shoulda Matchersの設定
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.test_framework :rspec
with.library :rails
end
end
config.use_transactional_fixtures = trueは、各テストをトランザクションで囲み、テスト終了後に自動でロールバックします。これにより、テスト間でデータが干渉しません。システムテストでは別途Database Cleanerが必要になる場合があります。
Step 3describe / context / it の構造
RSpecのテストは、describe、context、itの3つのブロックで階層的に構造化します。
| ブロック | 役割 | 使い方 |
|---|---|---|
describe |
テスト対象を定義 | クラス名やメソッド名を指定 |
context |
条件・状況を分類 | 「〜の場合」という条件を記述 |
it |
期待する振る舞いを記述 | 具体的なテストケース1件 |
RSpec.describe User, type: :model do
# テスト対象のクラス
describe "#full_name" do
# テスト対象のメソッド(インスタンスメソッドは#、クラスメソッドは.)
context "姓と名が設定されている場合" do
it "姓名を結合して返す" do
user = User.new(first_name: "太郎", last_name: "山田")
expect(user.full_name).to eq("山田 太郎")
end
end
context "名が空の場合" do
it "姓のみを返す" do
user = User.new(first_name: "", last_name: "山田")
expect(user.full_name).to eq("山田")
end
end
end
end
describeの引数にクラスを直接渡すと、described_classで参照できます。文字列でメソッド名を渡すときは、インスタンスメソッドは#method_name、クラスメソッドは.method_nameの慣例に従いましょう。
テストで共通のセットアップが必要な場合は、beforeブロックやletを使います。
RSpec.describe Article, type: :model do
# let は遅延評価(初めて呼ばれたときに実行される)
let(:user) { create(:user) }
let(:article) { create(:article, author: user) }
# let! は即時評価(beforeと同じタイミングで実行される)
let!(:published_article) { create(:article, :published) }
# before は各テストの前に実行される
before do
# テスト共通のセットアップ
end
describe "#publish!" do
it "記事を公開状態にする" do
article.publish!
expect(article).to be_published
end
end
end
letとlet!の違いに注意してください。letは遅延評価のため、テスト内で呼び出さなければ実行されません。テスト前にデータが存在している必要がある場合(例えば一覧表示のテスト)はlet!を使いましょう。
Step 4expect とマッチャー
RSpecではexpectを使って期待する結果を検証します。さまざまなマッチャーが用意されており、テストの意図を明確に表現できます。
基本のマッチャー
# 等値比較
expect(user.name).to eq("山田太郎")
# 真偽値
expect(user.active?).to be true
expect(user.admin?).to be false
# nil チェック
expect(user.deleted_at).to be_nil
# 述語マッチャー(?で終わるメソッドに対応)
expect(user).to be_active # user.active? が true
expect(user).to be_admin # user.admin? が true
# 包含チェック
expect(users).to include(user)
expect(user.roles).to include("editor")
# 文字列のパターンマッチ
expect(user.email).to match(/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i)
# 配列のサイズ
expect(users).to have_attributes(size: 3)
expect(users.size).to eq(3)
# 否定(to_not または not_to)
expect(user.name).not_to be_empty
変化を検証するマッチャー
# レコード数の変化
expect { User.create!(name: "test") }.to change(User, :count).by(1)
# 属性の変化
expect { user.activate! }.to change(user, :active).from(false).to(true)
# 例外の発生
expect { user.save! }.to raise_error(ActiveRecord::RecordInvalid)
# 出力のチェック
expect { puts "hello" }.to output("hello
").to_stdout
Shoulda Matchers(バリデーション・アソシエーション)
Shoulda Matchersを使うと、Railsのバリデーションやアソシエーションを1行でテストできます。
RSpec.describe User, type: :model do
# バリデーションのテスト
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it { is_expected.to validate_length_of(:password).is_at_least(8) }
it { is_expected.to validate_numericality_of(:age).is_greater_than(0) }
# アソシエーションのテスト
it { is_expected.to have_many(:articles).dependent(:destroy) }
it { is_expected.to belong_to(:company).optional }
it { is_expected.to have_one(:profile) }
it { is_expected.to have_many(:roles).through(:user_roles) }
end
is_expectedはexpect(subject)の省略形です。describe Userと書くと、subjectは自動的にUser.newになります。これによりShoulda Matchersのワンライナーが実現できます。
Step 5Factory Bot でテストデータ作成
Factory Botは、テスト用のデータを簡潔に作成するためのライブラリです。フィクスチャ(YAML)と比べて柔軟性が高く、必要なデータだけをカスタマイズできます。
ファクトリの定義
FactoryBot.define do
factory :user do
name { "テストユーザー" }
sequence(:email) { |n| "user#{n}@example.com" }
password { "password123" }
active { true }
# トレイト(特定の状態を定義)
trait :admin do
admin { true }
end
trait :inactive do
active { false }
end
# アソシエーション
trait :with_articles do
transient do
articles_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:article, evaluator.articles_count, author: user)
end
end
end
end
FactoryBot.define do
factory :article do
sequence(:title) { |n| "テスト記事 #{n}" }
body { Faker::Lorem.paragraphs(number: 3).join("
") }
association :author, factory: :user
trait :published do
status { "published" }
published_at { Time.current }
end
trait :draft do
status { "draft" }
published_at { nil }
end
end
end
ファクトリの使い方
# DBに保存してインスタンスを返す
user = create(:user)
# DBに保存せずインスタンスを返す(バリデーションテスト向き)
user = build(:user)
# 属性のハッシュを返す
attrs = attributes_for(:user)
# トレイトを指定
admin = create(:user, :admin)
inactive_user = create(:user, :inactive)
# 属性を上書き
user = create(:user, name: "カスタム名前")
# 複数作成
users = create_list(:user, 5)
# トレイトの組み合わせ
admin = create(:user, :admin, :with_articles, articles_count: 10)
バリデーションのテストではbuildを使い、DBへの保存が必要なテストではcreateを使い分けましょう。createはDBアクセスが発生するため、不要に使うとテストが遅くなります。
Step 6モデルスペック・コントローラスペックの実例
ここまで学んだ知識を組み合わせて、実践的なスペックを書いてみましょう。
モデルスペックの実例
require "rails_helper"
RSpec.describe Article, type: :model do
# --- バリデーション ---
describe "バリデーション" do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:body) }
it { is_expected.to validate_length_of(:title).is_at_most(255) }
end
# --- アソシエーション ---
describe "アソシエーション" do
it { is_expected.to belong_to(:author).class_name("User") }
it { is_expected.to have_many(:comments).dependent(:destroy) }
it { is_expected.to have_many(:tags).through(:article_tags) }
end
# --- スコープ ---
describe ".published" do
let!(:published) { create(:article, :published) }
let!(:draft) { create(:article, :draft) }
it "公開済みの記事のみ返す" do
expect(Article.published).to include(published)
expect(Article.published).not_to include(draft)
end
end
# --- インスタンスメソッド ---
describe "#publish!" do
let(:article) { create(:article, :draft) }
it "ステータスをpublishedに変更する" do
expect { article.publish! }.to change(article, :status)
.from("draft").to("published")
end
it "published_atを設定する" do
freeze_time do
article.publish!
expect(article.published_at).to eq(Time.current)
end
end
end
# --- クラスメソッド ---
describe ".search" do
let!(:ruby_article) { create(:article, title: "Ruby入門") }
let!(:rails_article) { create(:article, title: "Rails入門") }
it "タイトルに一致する記事を返す" do
results = Article.search("Ruby")
expect(results).to include(ruby_article)
expect(results).not_to include(rails_article)
end
it "空文字の場合は全件返す" do
expect(Article.search("").count).to eq(2)
end
end
end
リクエストスペック(コントローラーテスト)の実例
Rails 5以降では、コントローラスペックの代わりにリクエストスペックが推奨されています。実際のHTTPリクエストに近い形でテストできます。
require "rails_helper"
RSpec.describe "Articles", type: :request do
let(:user) { create(:user) }
let(:admin) { create(:user, :admin) }
describe "GET /articles" do
let!(:articles) { create_list(:article, 3, :published) }
it "公開記事一覧を表示する" do
get articles_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(articles.first.title)
end
end
describe "GET /articles/:id" do
let(:article) { create(:article, :published) }
it "記事の詳細を表示する" do
get article_path(article)
expect(response).to have_http_status(:ok)
expect(response.body).to include(article.title)
end
context "存在しないIDの場合" do
it "404を返す" do
get article_path(id: 99999)
expect(response).to have_http_status(:not_found)
end
end
end
describe "POST /articles" do
let(:valid_params) do
{ article: attributes_for(:article) }
end
context "ログイン済みの場合" do
before { sign_in user }
it "記事を作成する" do
expect {
post articles_path, params: valid_params
}.to change(Article, :count).by(1)
end
it "作成後に記事ページへリダイレクトする" do
post articles_path, params: valid_params
expect(response).to redirect_to(article_path(Article.last))
end
end
context "未ログインの場合" do
it "ログインページへリダイレクトする" do
post articles_path, params: valid_params
expect(response).to redirect_to(new_user_session_path)
end
end
end
describe "DELETE /articles/:id" do
let!(:article) { create(:article, author: user) }
context "記事の作者の場合" do
before { sign_in user }
it "記事を削除する" do
expect {
delete article_path(article)
}.to change(Article, :count).by(-1)
end
end
context "他のユーザーの場合" do
let(:other_user) { create(:user) }
before { sign_in other_user }
it "403を返す" do
delete article_path(article)
expect(response).to have_http_status(:forbidden)
end
end
end
end
テストを実行するには、ターミナルで以下のコマンドを使います。
# 全テスト実行
$ bundle exec rspec
# 特定のファイルだけ実行
$ bundle exec rspec spec/models/article_spec.rb
# 特定の行だけ実行
$ bundle exec rspec spec/models/article_spec.rb:15
# タグで絞り込み
$ bundle exec rspec --tag focus
# 失敗したテストだけ再実行
$ bundle exec rspec --only-failures
まとめ
- テストはユニット・結合・システムの3種類があり、ユニットテストを中心に書く
- RSpecは
describe/context/itの階層構造でテストを整理する expectとマッチャーで期待する振る舞いを検証する- Factory Botでテストデータを柔軟に作成し、トレイトで状態を管理する
- モデルスペックではバリデーション・スコープ・メソッドをテストする
- リクエストスペックでHTTPレスポンスや認可を検証する
- Shoulda Matchersでバリデーションとアソシエーションを簡潔にテストする