🙌
【RSpec入門】PATCHリクエストのテストを完全理解!記事更新機能で学ぶデータ更新処理のテスト
PATCHリクエストって何?
前回はPOST(作成)について学びましたが、今回は**PATCH(更新)**について学びます。
身近な例で理解しよう
Twitter(X)の編集機能を想像してください:
- 📝 すでに投稿したツイートの「編集」ボタンをクリック
- ✏️ 内容を修正(例:誤字を直す)
- 📤 「更新」ボタンをクリック(PATCHリクエスト)
- ✅ ツイートが更新される
これがPATCHリクエストの流れです!
POSTとPATCHの違い
操作 | HTTPメソッド | 何をする? | 例 |
---|---|---|---|
POST | POST | 新しいデータを作る | 新しい記事を投稿 |
PATCH | PATCH | 既存のデータを更新 | 記事の内容を修正 |
コントローラーの実装を理解しよう
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def update
# 1. 更新したい記事をIDで検索
@article = Article.find(params[:id])
# 2. 送られてきたデータで更新を試みる
if @article.update(article_params)
# 成功:更新された記事データを返す
render json: @article
else
# 失敗:エラー内容を返す
render json: @article.errors, status: :unprocessable_entity
end
end
private
def article_params
# Strong Parameters:安全な項目だけを許可
params.require(:article).permit(:title, :content)
end
end
コードを1行ずつ解説
1. 更新対象の記事を検索
@article = Article.find(params[:id])
-
params[:id]
:URLの:id
部分(例:/articles/123
の123
) -
Article.find
:指定されたIDの記事をデータベースから検索 - 見つからない場合は
ActiveRecord::RecordNotFound
エラーが発生
具体例:
PATCH /articles/5 # → params[:id] = "5"
Article.find("5") # → ID=5の記事を検索
2. データの更新処理
if @article.update(article_params)
-
@article.update
:既存の記事データを更新 -
article_params
:Strong Parametersでフィルタリングされた安全なデータ - 更新成功なら
true
、失敗ならfalse
を返す
3. 成功時の処理
render json: @article
-
render json:
:更新された記事データをJSONで返す - ステータスコードは指定なしの場合、200(OK)が自動設定される
4. 失敗時の処理
render json: @article.errors, status: :unprocessable_entity
-
@article.errors
:バリデーションエラーの詳細情報 -
status: :unprocessable_entity
:HTTPステータス422(データに問題あり)
テストの実装:正常系
# spec/requests/articles_spec.rb
RSpec.describe "Articles", type: :request do
describe "PATCH /articles/:id" do
# テスト用の記事を事前に作成
let(:article) { Article.create!(title: "古い記事", content: "これは古い記事の内容です。") }
context '有効なパラメータの場合' do
it '記事を更新できる' do
# PATCHリクエストを送信
patch "/articles/#{article.id}", params: {
article: {
title: "更新された記事",
content: "これは更新された記事の内容です。"
}
}
# レスポンスのステータスをチェック
expect(response).to have_http_status(:ok) # 200
# レスポンスの内容をチェック
json = JSON.parse(response.body)
expect(json['title']).to eq('更新された記事')
expect(json['content']).to eq('これは更新された記事の内容です。')
# データベースの実際のデータもチェック
article.reload
expect(article.title).to eq('更新された記事')
expect(article.content).to eq('これは更新された記事の内容です。')
end
end
end
end
テストコードを1行ずつ解説
1. テスト用データの準備
let(:article) { Article.create!(title: "古い記事", content: "これは古い記事の内容です。") }
-
let
:テストで使用する変数を定義(遅延評価) -
Article.create!
:記事をデータベースに実際に作成 -
!
がついている理由:作成に失敗した場合にエラーを発生させる
なぜletを使う?
# ❌ beforeだと、使わないテストでも必ず実行される
before { @article = Article.create!(...) }
# ✅ letだと、使用するテストでのみ実行される
let(:article) { Article.create!(...) }
2. PATCHリクエストの送信
patch "/articles/#{article.id}", params: { article: { ... } }
-
patch
:PATCHリクエストを送信するメソッド -
"/articles/#{article.id}"
:URLにIDを埋め込み(例:/articles/5
) -
params:
:更新用のデータ
実際のHTTPリクエストに換算すると:
PATCH /articles/5
Content-Type: application/json
{
"article": {
"title": "更新された記事",
"content": "これは更新された記事の内容です。"
}
}
3. レスポンスの検証
expect(response).to have_http_status(:ok)
- 更新成功時は
200
(OK)が返される - 作成時の
201
(Created)とは異なるので注意
4. 最重要!データベースの確認
article.reload
expect(article.title).to eq('更新された記事')
-
article.reload
:データベースから最新の状態を再読み込み - これを忘れると、メモリ上の古いデータでテストしてしまう!
なぜreloadが必要?
# テスト開始時
article = Article.create!(title: "古い記事", ...) # メモリ上のオブジェクト
# PATCHリクエストでデータベースは更新される
patch "/articles/#{article.id}", params: { ... }
# でも、メモリ上のarticleオブジェクトは古いまま!
puts article.title # => "古い記事"(更新されていない)
# reloadでデータベースから最新データを取得
article.reload
puts article.title # => "更新された記事"(最新の状態)
異常系のテスト:バリデーションエラー
RSpec.describe "Articles", type: :request do
describe "PATCH /articles/:id" do
let(:article) { Article.create!(title: "古い記事", content: "これは古い記事の内容です。") }
context '無効なパラメータの場合' do
it '記事を更新できない' do
# 更新前の値を保存
original_title = article.title
original_content = article.content
# 無効なデータで更新を試みる
patch "/articles/#{article.id}", params: {
article: {
title: "", # 空文字(バリデーションエラー)
content: "新しい内容"
}
}
# エラー時のステータスコードをチェック
expect(response).to have_http_status(:unprocessable_entity) # 422
# エラーメッセージの確認
json = JSON.parse(response.body)
expect(json['title']).to include("can't be blank")
# データベースの内容が変更されていないことを確認
article.reload
expect(article.title).to eq(original_title) # 元のタイトルのまま
expect(article.content).to eq(original_content) # 元の内容のまま
end
end
end
end
異常系テストのポイント
1. 元の値を保存
original_title = article.title
original_content = article.content
- 更新前の値を変数に保存
- 後で「更新されていないこと」を確認するため
2. 部分的に無効なデータ
patch "/articles/#{article.id}", params: {
article: {
title: "", # ❌ 無効(空文字)
content: "新しい内容" # ✅ 有効
}
}
- 一部だけ無効なデータを送信
- 重要:有効な部分も更新されないことを確認
3. データが変更されていないことを確認
article.reload
expect(article.title).to eq(original_title)
expect(article.content).to eq(original_content)
-
reload
で最新状態を取得 - 元の値と比較して、変更されていないことを確認
異常系のテスト:存在しない記事
RSpec.describe "Articles", type: :request do
describe "PATCH /articles/:id" do
context '存在しない記事の場合' do
it '404エラーが返される' do
# 存在しないIDを指定
patch "/articles/99999", params: {
article: {
title: "更新されるはずの記事",
content: "この更新は実行されません。"
}
}
# 404エラーが返されることを確認
expect(response).to have_http_status(:not_found) # 404
end
end
end
end
なぜ404になる?
@article = Article.find(params[:id]) # ID=99999の記事を検索
# → 見つからないので ActiveRecord::RecordNotFound エラー
# → Railsが自動で404ステータスを返す
部分更新のテスト
PATCHの強みは「部分更新」です。一部のフィールドだけ更新できます。
it 'タイトルのみ更新できる' do
original_content = article.content
# タイトルだけを更新
patch "/articles/#{article.id}", params: {
article: {
title: "新しいタイトル"
# contentは送信しない
}
}
expect(response).to have_http_status(:ok)
article.reload
expect(article.title).to eq('新しいタイトル') # 更新された
expect(article.content).to eq(original_content) # 変更されていない
end
部分更新の流れ
-
元のデータ:
{ title: "古い記事", content: "古い内容" }
-
送信データ:
{ title: "新しいタイトル" }
-
結果:
{ title: "新しいタイトル", content: "古い内容" }
PUTとPATCHの違い:
- PUT:全項目を置き換え(送信しなかった項目は消える)
- PATCH:指定した項目だけ更新(送信しなかった項目はそのまま)
よくある間違いとトラブルシューティング
1. reloadを忘れる(超頻出ミス!)
# ❌ 間違ったテスト
it '記事を更新できる' do
patch "/articles/#{article.id}", params: { ... }
# reloadなしでチェック
expect(article.title).to eq('更新された記事') # ❌ 古いデータを見ている
end
# ✅ 正しいテスト
it '記事を更新できる' do
patch "/articles/#{article.id}", params: { ... }
article.reload # データベースから最新データを取得
expect(article.title).to eq('更新された記事') # ✅ 最新データを確認
end
2. ステータスコードの間違い
# ❌ 間違ったステータス期待
expect(response).to have_http_status(:created) # 201は作成時のステータス
# ✅ 正しいステータス期待
expect(response).to have_http_status(:ok) # 200は更新時のステータス
3. IDの指定忘れ
# ❌ IDがないとエラー
patch "/articles", params: { ... } # どの記事を更新するの?
# ✅ IDを明確に指定
patch "/articles/#{article.id}", params: { ... } # ID=5の記事を更新
実践的なテストパターン
パターン1:更新前後の比較テスト
it '更新前後で正しく変更される' do
# 更新前の状態を記録
expect(article.title).to eq('古い記事')
expect(article.content).to eq('これは古い記事の内容です。')
# 更新実行
patch "/articles/#{article.id}", params: {
article: {
title: "完全に新しいタイトル",
content: "完全に新しい内容でとても長い文章です。"
}
}
# 更新後の状態を確認
article.reload
expect(article.title).to eq('完全に新しいタイトル')
expect(article.content).to eq('完全に新しい内容でとても長い文章です。')
# 更新前と異なることを明示的に確認
expect(article.title).not_to eq('古い記事')
expect(article.content).not_to eq('これは古い記事の内容です。')
end
パターン2:タイムスタンプの更新確認
it '更新日時が正しく更新される' do
original_updated_at = article.updated_at
# 時間を少し進める(確実に更新日時が変わるように)
travel 1.second do
patch "/articles/#{article.id}", params: {
article: { title: "タイムスタンプテスト" }
}
end
article.reload
expect(article.updated_at).to be > original_updated_at # 更新日時が新しくなった
expect(article.title).to eq('タイムスタンプテスト')
end
travelって何?
- テスト用の時間操作メソッド
-
travel 1.second
:時間を1秒進める - 更新日時のテストで重宝します
パターン3:複数フィールドの部分更新
it '一部のフィールドのみ更新される' do
original_content = article.content # 元の内容を保存
# タイトルだけ更新(contentは送信しない)
patch "/articles/#{article.id}", params: {
article: {
title: "タイトルだけ変更"
# contentは意図的に送信しない
}
}
article.reload
expect(article.title).to eq('タイトルだけ変更') # 更新された
expect(article.content).to eq(original_content) # 変更されていない
end
異常系のテスト:詳細版
context '無効なパラメータの場合' do
it '記事を更新できずエラーが返される' do
# 更新前の値を全て保存
original_title = article.title
original_content = article.content
original_updated_at = article.updated_at
# 無効なデータで更新を試みる
patch "/articles/#{article.id}", params: {
article: {
title: "", # ❌ 空文字(必須項目違反)
content: "短い" # ❌ 10文字未満(長さ制限違反)
}
}
# エラーレスポンスの確認
expect(response).to have_http_status(:unprocessable_entity) # 422
# エラーメッセージの詳細確認
json = JSON.parse(response.body)
expect(json['title']).to include("can't be blank") # タイトルエラー
expect(json['content']).to include("is too short") # 内容エラー
# データベースが変更されていないことを確認
article.reload
expect(article.title).to eq(original_title) # 元のタイトルのまま
expect(article.content).to eq(original_content) # 元の内容のまま
expect(article.updated_at).to eq(original_updated_at) # 更新日時も変わっていない
end
end
存在しない記事の更新テスト
context '存在しない記事の場合' do
it '404エラーが返される' do
# 存在しないID(99999)を指定
patch "/articles/99999", params: {
article: {
title: "更新されるはずの記事",
content: "この更新は実行されません。最低10文字以上の内容です。"
}
}
# 404エラーが返されることを確認
expect(response).to have_http_status(:not_found) # 404
end
end
エラーの流れ
-
Article.find(99999)
を実行 - ID=99999の記事が見つからない
-
ActiveRecord::RecordNotFound
エラーが発生 - Railsが自動で404レスポンスを返す
実際のエラーレスポンス例
バリデーションエラー時
リクエスト:
{
"article": {
"title": "",
"content": "短い"
}
}
レスポンス(ステータス422):
{
"title": ["can't be blank"],
"content": ["is too short (minimum is 10 characters)"]
}
記事が見つからない時
リクエスト:
PATCH /articles/99999
レスポンス(ステータス404):
{
"error": "Record not found"
}
テストで確認すべきチェックリスト
✅ 正常系で確認すること
- HTTPステータス:200(OK)が返るか?
- レスポンス内容:更新後のデータが正しく返るか?
- データベース:実際にDBのデータが更新されたか?
- 部分更新:指定しなかった項目は変更されないか?
✅ 異常系で確認すること
- バリデーションエラー:422ステータスが返るか?
- エラーメッセージ:分かりやすいメッセージか?
- データ保護:無効な更新でDBが変更されないか?
- 存在しない記事:404ステータスが返るか?
PATCHとPOSTの違いをテストで比較
項目 | POST(作成) | PATCH(更新) |
---|---|---|
URL | /articles |
/articles/:id |
目的 | 新規作成 | 既存データ更新 |
成功ステータス |
201 (Created) |
200 (OK) |
データ変化 |
count が+1 |
count は変わらず |
テスト対象記事 | 不要 | 事前に作成が必要 |
まとめ
PATCHリクエストのテストで重要なポイント:
-
テスト用データの準備:
let
で更新対象を作成 -
URLにIDを含める:
/articles/#{article.id}
- reloadでデータベース確認:メモリとDBの状態は別物
- 3つのシナリオ:正常系・バリデーションエラー・存在しない記事
- 適切なステータス:成功時200、エラー時422、見つからない時404
覚えておきたい魔法の呪文
# データ準備
let(:article) { Article.create!(title: "古いタイトル", content: "古い内容で十分な長さがあります。") }
# 更新実行
patch "/articles/#{article.id}", params: { article: { title: "新しいタイトル" } }
# 確認の三点セット
expect(response).to have_http_status(:ok) # ステータス確認
json = JSON.parse(response.body) # レスポンス解析
article.reload # DB状態更新
次回は、DELETEリクエスト(削除処理)のテストについて学びます。
更新まで理解できれば、削除はもっと簡単ですよ!
Happy Testing! 🎉
Discussion