第22章|今さら学ぶ「テスト」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- なぜテストを書くのか — 「安全ネット」としてのテスト
- 単体 / 統合 / E2E テストの違い
- TDDの流れ — Red → Green → Refactor の信号機
- RSpec と Minitest — 2つのテストフレームワーク
- FactoryBot — テスト用データの工場
🏠 たとえ話で掴む「テスト」
テストは 自動車の検査 にたとえるとわかりやすいです。
車を作ったら、いきなり公道に出しません。まず部品ごとの検査(ブレーキは効くか、エンジンは動くか)をして、次に組み立てた状態の検査(走れるか、曲がれるか)をして、最後に実際の道路を想定した試運転をします。
| 自動車の検査 | テストの種類 |
|---|---|
| 部品検査(ブレーキ単体) | 単体テスト (モデル単体) |
| 組み立て検査(ブレーキ+車体) | 統合テスト (コントローラ+モデル) |
| 試運転(実際の道路で走る) | E2Eテスト (ブラウザで操作) |
テストを書いておくと、コードを変更したときに「壊れていないか」を一瞬で確認できます。手動で全画面を確認するのは非現実的ですが、テストなら数秒〜数分で完了します。
🧪 テストとは何か — 技術的な定義
テストとは、 コードが期待通りに動くことを、別のコードで自動的に検証する仕組み です。
手動テスト(画面を触って確認)との決定的な違いは、 繰り返し実行できる 点にあります。アプリの機能が10個のうちは手動でも確認できますが、100個、500個と増えたとき、1つの変更のたびに全機能を手作業で確認するのは不可能です。
テストを書く理由は大きく3つあります。
- リグレッション(退行)の防止 — 新しいコードを追加したとき、既存の機能が壊れていないことを自動で確認できる
- 仕様の文書化 — テストコード自体が「この機能はこう動くべき」という仕様書の役割を果たす
- リファクタリングの安全網 — テストが通る限り、安心してコードの構造を変えられる(→ 第25章)
テストがない状態でコードを変更するのは、安全ネットなしで綱渡りをするのと同じです。
📊 テストの3つの層
単体テスト(Unit Test)
モデルのバリデーションやメソッドなど、 1つの部品 をテストします。
# spec/models/article_spec.rb
RSpec.describe Article, type: :model do
describe "バリデーション" do
it "タイトルと本文があれば有効" do
article = build(:article) # FactoryBotで作成
expect(article).to be_valid
end
it "タイトルがなければ無効" do
article = build(:article, title: "")
expect(article).not_to be_valid
expect(article.errors[:title]).to include("を入力してください")
end
it "タイトルが100文字を超えると無効" do
article = build(:article, title: "a" * 101)
expect(article).not_to be_valid
end
end
describe "#liked_by?" do
it "いいね済みならtrueを返す" do
article = create(:article)
user = create(:user)
create(:like, user: user, likeable: article)
expect(article.liked_by?(user)).to be true
end
end
end
統合テスト(Request Spec / Integration Test)
コントローラの リクエスト〜レスポンス をテストします。
# spec/requests/articles_spec.rb
RSpec.describe "Articles", type: :request do
describe "GET /articles" do
it "公開記事の一覧が表示される" do
create(:article, status: :published, title: "Ruby入門")
get articles_path
expect(response).to have_http_status(:success)
expect(response.body).to include("Ruby入門")
end
end
describe "POST /articles" do
context "ログイン済みの場合" do
it "記事が作成される" do
user = create(:user)
sign_in(user) # spec/support/auth_helpers.rb に定義したカスタムヘルパー
expect {
post articles_path, params: { article: { title: "新記事", body: "本文テスト" } }
}.to change(Article, :count).by(1)
expect(response).to redirect_to(article_path(Article.last))
end
end
context "未ログインの場合" do
it "ログインページにリダイレクトされる" do
post articles_path, params: { article: { title: "新記事", body: "本文テスト" } }
expect(response).to redirect_to(new_session_path)
end
end
end
end
E2Eテスト(System Spec)
実際のブラウザ操作 をシミュレートするテストです。
# spec/system/articles_spec.rb
RSpec.describe "記事の投稿", type: :system do
it "ログインして記事を投稿できる" do
user = create(:user)
visit new_session_path
fill_in "メールアドレス", with: user.email_address
fill_in "パスワード", with: "password"
click_button "ログイン"
visit new_article_path
fill_in "タイトル", with: "RSpecの使い方"
fill_in "本文", with: "RSpecはRubyのテストフレームワークです。"
click_button "投稿する"
expect(page).to have_content("記事を投稿しました")
expect(page).to have_content("RSpecの使い方")
end
end
テストピラミッド
/\
/ \ E2Eテスト(少数。重要なフローだけ)
/ \
/──────\
/ \ 統合テスト(中程度。各エンドポイント)
/ \
/────────────\
/ \ 単体テスト(大量。モデルのバリデーション・メソッド)
/________________\
下にいくほど:速い、安定、数が多い
上にいくほど:遅い、壊れやすい、数が少ない
なぜこの比率なのかというと、単体テストは外部依存(DB・ブラウザ)が少なく高速に実行でき、テストの失敗原因も特定しやすいためです。E2Eテストはユーザー視点の検証ができる反面、ブラウザの起動が必要で遅く、CSSの変更などでテストが壊れやすいという特徴があります。
🚦 TDD — Red → Green → Refactor
TDD(テスト駆動開発) は、「テストを先に書いてからコードを書く」開発手法です。
① Red(赤) — まずテストを書く → 当然失敗する(赤)
② Green(緑) — テストが通る最小限のコードを書く(緑)
③ Refactor — コードをきれいにする(テストが緑のまま)
④ 繰り返す
# ① Red — テストを先に書く
it "記事のステータスがdraftならtrue" do
article = build(:article, status: :draft)
expect(article.draft?).to be true # まだ enum 未定義 → 失敗(Red)
end
# ② Green — 通る最小限のコードを書く
class Article < ApplicationRecord
enum :status, { draft: 0, published: 1 } # 追加
end
# → テスト通る(Green)
# ③ Refactor — 必要ならリファクタリング
最初は面倒に感じるが、慣れると「何を実装すべきか」が明確になり、結果的に開発が速くなるケースが多いです。テストが失敗した原因を調べるデバッグ手法については(→ 第23章で詳しく扱います)。
🔧 RSpec と Minitest
Minitest — Railsのデフォルト
# test/models/article_test.rb
class ArticleTest < ActiveSupport::TestCase
test "タイトルがなければ無効" do
article = Article.new(body: "本文")
assert_not article.valid?
assert_includes article.errors[:title], "を入力してください"
end
end
RSpec — 実務で最も使われるフレームワーク
# spec/models/article_spec.rb
RSpec.describe Article, type: :model do
it "タイトルがなければ無効" do
article = build(:article, title: "")
expect(article).not_to be_valid
expect(article.errors[:title]).to include("を入力してください")
end
end
| Minitest | RSpec | |
|---|---|---|
| 導入 | Railsデフォルト(追加不要) | Gemで追加 |
| 記法 |
assert_xxx(アサーション式) |
expect(xxx).to(自然言語風) |
| 実務採用率 | 約30〜40% | 約55〜70% |
| 学習コスト | 低い | やや高い(DSLが多い) |
| 表現力 | シンプル | 豊富(describe/context/it) |
KnowledgeNoteでは RSpec をメイン に採用します。
🏭 FactoryBot — テスト用データの工場
FactoryBot は、テスト用のデータを簡単に作るためのGemです。
# spec/factories/users.rb
FactoryBot.define do
factory :user do
name { Faker::Name.name }
sequence(:email_address) { |n| "user#{n}@example.com" }
password { "password" }
password_confirmation { "password" }
end
end
# spec/factories/articles.rb
FactoryBot.define do
factory :article do
title { Faker::Lorem.sentence(word_count: 5) }
body { Faker::Lorem.paragraphs(number: 3).join("\n\n") }
status { :published }
association :user # userファクトリと自動で紐づく
end
end
# 使い方
build(:user) # メモリ上にUserを作る(DBに保存しない)
create(:user) # DBに保存する
create(:user, name: "田中") # 属性を上書き
create_list(:article, 5) # 5件まとめて作る
build と create の使い分け
| メソッド | DBに保存 | 速度 | 使いどころ |
|---|---|---|---|
build |
しない | 速い | バリデーションのテスト |
create |
する | 遅い | DB参照が必要なテスト |
テストの速度を保つために、 DBに保存する必要がなければ build を使う のがベストプラクティスです。
let と let! の違い
RSpecでよく使う let と let! には重要な違いがあります。
# let — 遅延評価(実際に使われるまで実行されない)
let(:user) { create(:user) }
# let! — 即時評価(each の前に必ず実行される)
let!(:article) { create(:article, user: user) }
# 使い分けの例
RSpec.describe Article, type: :model do
let(:user) { create(:user) }
# ❌ let だと article が作られるのは it ブロック内で article 変数を参照した時
# → この例では expect(Article.count) の中で article 変数を参照していないため
# create が実行されず、count の変化を検知できない
let(:article) { create(:article, user: user) }
it "記事が存在する" do
expect(Article.count).to eq(1) # 失敗!(まだ作られていない)
end
# ✅ let! なら it ブロックの前に article が作られる
let!(:article) { create(:article, user: user) }
it "記事が存在する" do
expect(Article.count).to eq(1) # 成功
end
end
基本は let(遅延評価)を使い、 テスト実行前にデータが存在している必要がある場合だけ let! を使います。
🛠️ KnowledgeNoteでの具体例
# spec/factories/likes.rb
FactoryBot.define do
factory :like do
association :user
association :likeable, factory: :article # ポリモーフィック
end
end
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
describe "#follow / #unfollow" do
let(:tanaka) { create(:user) }
let(:suzuki) { create(:user) }
it "フォロー/アンフォローできる" do
tanaka.follow(suzuki)
expect(tanaka.following?(suzuki)).to be true
tanaka.unfollow(suzuki)
expect(tanaka.following?(suzuki)).to be false
end
it "自分自身はフォローできない" do
tanaka.follow(tanaka)
expect(tanaka.following?(tanaka)).to be false
end
end
end
💼 面接で聞かれたら?
Q:テストの種類と使い分けを説明してください。
「テストは大きく3層に分かれます。単体テストはモデルのバリデーションやメソッドなど部品単位のテスト、統合テストはコントローラのリクエスト〜レスポンスを検証するテスト、E2Eテストはブラウザ操作をシミュレートするテストです。テストピラミッドの考え方に基づき、単体テストを多く・E2Eテストを少なく書くのが効率的です。単体テストは速くて安定しており、E2Eテストは遅いがユーザー視点の検証ができます。」
深掘りされたら:
- 「TDDとは?」→ テストを先に書いてからコードを実装する手法。Red(テスト失敗)→ Green(最小限の実装で通す)→ Refactor(きれいにする)のサイクルを繰り返す。
- 「FactoryBotのbuildとcreateの違いは?」→ buildはメモリ上のみ(DB保存なし)で高速。createはDBに保存する。バリデーションテストにはbuild、関連データのテストにはcreateを使い分ける。
- 「letとlet!の違いは?」→ letは遅延評価で、テスト内で参照されるまで実行されない。let!は即時評価で、itブロックの前に必ず実行される。テスト前にデータが必要な場面ではlet!を使う。
🔗 もっと深く知りたい人へ(1次情報リンク)
- RSpec 公式ドキュメント — RSpecの全機能
- FactoryBot(GitHub) — FactoryBotの公式ガイド
- Rails ガイド:Rails テスティングガイド — Minitest中心のRails公式テストガイド
- Capybara(GitHub) — E2Eテスト用のブラウザ操作ライブラリ
まとめ
- ✅ テストは「自動車検査」。部品検査(単体)→ 組み立て検査(統合)→ 試運転(E2E)の3層
- ✅ テストを書く理由はリグレッション防止・仕様の文書化・リファクタリングの安全網の3つ
- ✅ テストピラミッド:単体テストを多く、E2Eテストを少なく
- ✅ TDD:テストを先に書く → Red → Green → Refactor のサイクル
- ✅ RSpecが実務で最も使われる。Minitestはデフォルト
- ✅ FactoryBotでテストデータを効率的に作る。build / create / let / let! を使い分ける
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに