🔖
RSpecで`change`を使うべき場面とは?`eq`より優れている理由を解説
change
を使うべき場面とは?eq
より優れている理由を徹底解説
RSpecでRSpecでテストを書いていると、「この場面ではeq
とchange
どちらを使うべき?」と悩むことがありませんか?この記事では、change
メソッドが威力を発揮する場面と、なぜeq
やbe
よりも適しているケースがあるのかを具体例とともに解説します。
TL;DR(要約)
-
change
: 実行前後での値の変化を検証(副作用のテスト) -
eq
/be
: 現在の値や状態を検証(結果のテスト) -
change
が適している場面: データベースへの保存、カウンターの増減、配列への要素追加など
change
メソッドとは?
change
メソッドは、何かの処理を実行する前後で値が変化したかどうかをテストするマッチャーです。
# 基本的な書き方
expect { 何かの処理 }.to change { 監視したい値 }
eq
だけでは不十分なのか?
なぜ
eq
だけを使った場合の問題点
❌ RSpec.describe "ユーザー作成のテスト" do
it "ユーザーが作成される" do
# これだけでは不十分!
user = User.create(name: "太郎")
expect(user.name).to eq("太郎")
# 問題:本当にデータベースに保存されたかわからない
# 問題:他のユーザーが削除されていないかわからない
# 問題:ユーザー数が正しく増加したかわからない
end
end
change
を使った場合の利点
✅ RSpec.describe "ユーザー作成のテスト" do
it "ユーザーが作成される" do
# 副作用もしっかりテスト
expect {
User.create(name: "太郎")
}.to change { User.count }.by(1)
# 作成されたユーザーの内容も確認
expect(User.last.name).to eq("太郎")
end
end
change
が威力を発揮する具体例
1. データベースのレコード数の変化
RSpec.describe UsersController do
describe "POST #create" do
let(:user_params) { { name: "太郎", email: "taro@example.com" } }
it "ユーザーが1人増える" do
expect {
post :create, params: { user: user_params }
}.to change { User.count }.by(1)
end
it "管理者ユーザーが1人増える" do
admin_params = user_params.merge(role: "admin")
expect {
post :create, params: { user: admin_params }
}.to change { User.where(role: "admin").count }.by(1)
end
end
describe "DELETE #destroy" do
let!(:user) { User.create(name: "太郎") }
it "ユーザーが1人減る" do
expect {
delete :destroy, params: { id: user.id }
}.to change { User.count }.by(-1)
end
end
end
2. 関連するモデルの同時変更
RSpec.describe "記事の投稿" do
let(:user) { User.create(name: "太郎") }
it "記事とタグが正しく作成される" do
expect {
user.articles.create(
title: "Rails入門",
content: "Railsについて...",
tag_list: "Rails,Ruby"
)
}.to change { Article.count }.by(1)
.and change { Tag.count }.by(2)
.and change { user.articles.count }.by(1)
end
end
3. 状態の変化を伴う処理
RSpec.describe Order do
let(:order) { Order.create(status: "pending") }
describe "#ship!" do
it "ステータスが変更され、発送日が設定される" do
expect {
order.ship!
}.to change { order.status }.from("pending").to("shipped")
.and change { order.shipped_at }.from(nil)
end
end
describe "#complete!" do
let(:order) { Order.create(status: "shipped") }
it "注文が完了し、完了数がカウントアップされる" do
expect {
order.complete!
}.to change { order.status }.to("completed")
.and change { Order.completed.count }.by(1)
end
end
end
4. キューやバックグラウンドジョブ
RSpec.describe "メール送信" do
it "メール送信ジョブがキューに追加される" do
expect {
User.create(email: "test@example.com")
}.to change { ActionMailer::Base.deliveries.size }.by(1)
end
it "複数のジョブが適切にキューイングされる" do
expect {
10.times { |i| User.create(email: "user#{i}@example.com") }
}.to change { WelcomeEmailJob.jobs.size }.by(10)
end
end
5. ファイルシステムの操作
RSpec.describe FileUploader do
let(:uploader) { FileUploader.new }
it "ファイルがアップロードされる" do
file = fixture_file_upload("test.jpg", "image/jpeg")
expect {
uploader.upload(file)
}.to change { Dir.glob("uploads/*").count }.by(1)
end
it "古いファイルが削除される" do
# 事前に古いファイルを作成
old_file = uploader.upload(fixture_file_upload("old.jpg"))
expect {
uploader.cleanup_old_files
}.to change { File.exist?(old_file.path) }.from(true).to(false)
end
end
change
の様々な書き方
基本パターン
# 値が変化することを確認
expect { do_something }.to change { counter }
# 特定の値だけ変化することを確認
expect { do_something }.to change { counter }.by(1)
# from/toで具体的な変化を指定
expect { do_something }.to change { status }.from("pending").to("completed")
# 変化しないことを確認
expect { do_something }.not_to change { other_counter }
複数の変化を同時にテスト
expect {
user.articles.create(title: "新記事", published: true)
}.to change { user.articles.count }.by(1)
.and change { user.published_articles.count }.by(1)
.and change { user.updated_at }
ブロック内での詳細な確認
expect {
order = Order.create(items: [item1, item2])
}.to change { Order.count }.by(1)
# 作成されたオブジェクトの詳細も確認
created_order = Order.last
expect(created_order.items.count).to eq(2)
expect(created_order.total_amount).to eq(item1.price + item2.price)
change
を使うべき場面 vs 使わない場面
change
を使うべき場面
✅ - データベースへの保存・削除・更新
- カウンターや数値の増減
- 配列やハッシュへの要素追加・削除
- 状態の変化を伴う処理
- 外部サービスへの API コール数
- ログファイルへの書き込み
change
が不適切な場面
❌ # 単純な計算結果のテスト
expect(Calculator.add(2, 3)).to eq(5) # ✅ これでOK
# 以下は不自然
expect { result = Calculator.add(2, 3) }.to change { result } # ❌ 意味がない
実際の判断例
RSpec.describe ShoppingCart do
describe "#add_item" do
let(:cart) { ShoppingCart.new }
let(:item) { Item.new(name: "商品A", price: 1000) }
# ❌ eqだけでは不十分
it "商品が追加される(不十分なテスト)" do
cart.add_item(item)
expect(cart.items.last).to eq(item) # 追加されたかわからない
end
# ✅ changeで副作用もテスト
it "商品が追加される(完全なテスト)" do
expect {
cart.add_item(item)
}.to change { cart.items.count }.by(1)
.and change { cart.total_price }.by(1000)
expect(cart.items.last).to eq(item) # 内容も確認
end
end
end
パフォーマンスの考慮
change
メソッドは処理を2回実行するため、重い処理の場合は注意が必要です。
# 重い処理の場合は工夫が必要
RSpec.describe "大量データ処理" do
it "バッチ処理が正しく動作する" do
# before_countを事前に取得
before_count = ProcessedRecord.count
BatchProcessor.run
# 変化量を直接計算
expect(ProcessedRecord.count - before_count).to eq(1000)
end
end
まとめ
change
メソッドは、副作用を伴う処理のテストで真価を発揮します。
比較項目 |
eq /be
|
change |
---|---|---|
用途 | 現在の状態・値の確認 | 実行前後の変化の確認 |
適用場面 | 計算結果、オブジェクトの属性 | DB操作、状態変更、副作用 |
テストの観点 | 「結果が正しいか?」 | 「期待した変化が起きたか?」 |
良いテストは、何をテストしたいかを明確にすることから始まります。値そのものを確認したいならeq
、変化を確認したいならchange
を選びましょう。
適切なマッチャーを選ぶことで、テストの意図が明確になり、バグを見つけやすい堅牢なテストスイートが作れるようになります!
Discussion