👻

【RSpec入門】POSTリクエストのテストを理解!ブログ投稿機能で学ぶ実践的なテスト書き方

に公開

POSTリクエストって何?

まず基本から整理しましょう。

HTTPリクエストの種類

  • GET:データを「見る」(記事一覧を表示、記事詳細を表示)
  • POST:データを「作る」(新しい記事を投稿、ユーザー登録)
  • PATCH/PUT:データを「更新する」(記事の編集)
  • DELETE:データを「削除する」(記事の削除)

今回はPOST、つまり「新しいデータを作る」処理のテストを学びます。

身近な例で考えてみよう

Twitterで新しいツイートを投稿するとき:

  1. 📝 ツイート内容を入力
  2. 📤 「ツイート」ボタンをクリック(POSTリクエスト)
  3. ✅ サーバーで新しいツイートを保存
  4. 🎉 「投稿しました!」という画面が表示

これがPOSTリクエストの流れです!

今回作るもの:ブログ投稿機能

シンプルなブログサイトの記事投稿機能を作ってテストしてみましょう。

データベース設計(Articleモデル)

# app/models/article.rb
class Article < ApplicationRecord
  # バリデーション(データの検証ルール)
  validates :title, presence: true                    # タイトルは必須
  validates :content, presence: true, length: { minimum: 10 }  # 内容は必須かつ10文字以上
end

validatesって何?

  • presence: true:「この項目は空っぽじゃダメよ」
  • length: { minimum: 10 }:「最低10文字は書いてね」

コントローラーの実装を理解しよう

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def create
    # 1. 送られてきたデータでArticleを作る
    @article = Article.new(article_params)
    
    # 2. データベースに保存を試みる
    if @article.save
      # 成功した場合:作成した記事データを返す(ステータス201)
      render json: @article, status: :created
    else
      # 失敗した場合:エラー内容を返す(ステータス422)
      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.new(article_params)
  • article_params:安全にフィルタリングされたデータ
  • Article.new:新しい記事オブジェクトを作成(まだDBには保存されていない)

2. 保存処理

if @article.save
  • save:データベースに実際に保存を試みる
  • 成功すればtrue、失敗すればfalseを返す

3. 成功時の処理

render json: @article, status: :created
  • render json::JSONフォーマットでレスポンスを返す
  • status: :created:HTTPステータス201(新規作成成功)を返す

4. 失敗時の処理

render json: @article.errors, status: :unprocessable_entity
  • @article.errors:バリデーションエラーの詳細
  • status: :unprocessable_entity:HTTPステータス422(データに問題あり)

5. Strong Parameters

def article_params
  params.require(:article).permit(:title, :content)
end
  • require(:article):「articleという項目は必須だよ」
  • permit(:title, :content):「titleとcontentだけ許可するよ」

なぜStrong Parametersが必要?
悪意のあるユーザーがparamsに余計なデータ(例:admin: true)を混ぜる可能性があるからです。

テストの実装:正常系

# spec/requests/articles_spec.rb
RSpec.describe "Articles", type: :request do
  describe "POST /articles" do
    context '有効なパラメータの場合' do
      it '記事を作成できる' do
        # テスト前:データベースの記事数をチェック
        expect {
          post "/articles", params: {
            article: {
              title: "新しい記事",
              content: "この記事は新しく作成されました。"
            }
          }
        }.to change(Article, :count).by(1)  # 記事数が1増えることを期待

        # レスポンスのステータスをチェック
        expect(response).to have_http_status(201)  # 201 = 作成成功

        # レスポンスの内容をチェック
        json = JSON.parse(response.body)
        expect(json['title']).to eq('新しい記事')
        expect(json['content']).to eq('この記事は新しく作成されました。')
      end
    end
  end
end

コードを1つずつ解説

1. データベースの変化をチェック

expect {
  # この中でPOSTリクエストを実行
}.to change(Article, :count).by(1)
  • expect { }:ブロック内の処理の前後で何かが変わることを期待
  • change(Article, :count):Articleテーブルのレコード数の変化
  • .by(1):1増えることを期待

日本語に訳すと:「この処理を実行したら、記事の数が1個増えているはず」

2. POSTリクエストの送信

post "/articles", params: { article: { title: "...", content: "..." } }
  • post:POSTリクエストを送信するメソッド
  • "/articles":送信先のURL
  • params::送信するデータ

実際のHTTPリクエストに換算すると

POST /articles
Content-Type: application/json

{
  "article": {
    "title": "新しい記事",
    "content": "この記事は新しく作成されました。"
  }
}

3. レスポンスの検証

expect(response).to have_http_status(201)
  • response:サーバーからの返答
  • have_http_status(201):HTTPステータスコードが201か?

HTTPステータスコードって何?

  • 200:成功(OK)
  • 201:作成成功(Created)
  • 404:見つからない(Not Found)
  • 422:データに問題あり(Unprocessable Entity)
  • 500:サーバーエラー(Internal Server Error)

4. JSONデータの検証

json = JSON.parse(response.body)
expect(json['title']).to eq('新しい記事')
  • response.body:サーバーから返ってきたデータ(文字列)
  • JSON.parse:JSON文字列をRubyのHashに変換
  • json['title']:変換されたHashから値を取り出し

テストの実装:異常系

RSpec.describe "Articles", type: :request do
  describe "POST /articles" do
    context '無効なパラメータの場合' do
      it '記事を作成できない' do
        # データベースの記事数が変わらないことを期待
        expect {
          post "/articles", params: {
            article: {
              title: "",      # 空文字(バリデーションエラー)
              content: ""     # 空文字(バリデーションエラー)
            }
          }
        }.not_to change(Article, :count)  # 記事数は変わらない

        # エラー時のステータスコードをチェック
        expect(response).to have_http_status(422)  # 422 = データに問題あり

        # エラーメッセージの内容をチェック
        json = JSON.parse(response.body)
        expect(json['title']).to include("can't be blank")
        expect(json['content']).to include("can't be blank")
        expect(json['content']).to include("is too short")
      end
    end
  end
end

異常系テストのポイント

1. データが作成されないことを確認

expect { ... }.not_to change(Article, :count)
  • .not_to change:「変わらないことを期待」
  • エラー時にデータが作られてしまったら大問題!

2. 適切なエラーステータス

expect(response).to have_http_status(422)
  • 422:「あなたの送ったデータに問題があります」という意味
  • フロントエンド側で適切なエラー表示ができる

3. エラーメッセージの確認

expect(json['title']).to include("can't be blank")
  • ユーザーに分かりやすいエラーメッセージが返されているか?
  • フロントエンド側で具体的なエラー表示ができるか?

よくあるつまずきポイント

1. Strong Parametersの書き忘れ

# ❌ 危険なコード
def create
  @article = Article.new(params[:article])  # 生のparamsを使用
  # ...
end

# ✅ 安全なコード
def create
  @article = Article.new(article_params)  # Strong Parametersを使用
  # ...
end

private

def article_params
  params.require(:article).permit(:title, :content)
end

2. ステータスコードの間違い

# ❌ 間違ったステータス
render json: @article, status: :ok  # 200(更新成功時に使う)

# ✅ 正しいステータス
render json: @article, status: :created  # 201(作成成功時に使う)

3. バリデーションエラーの処理忘れ

# ❌ エラー処理なし
def create
  @article = Article.new(article_params)
  @article.save  # 失敗してもエラーハンドリングなし
  render json: @article
end

# ✅ 適切なエラー処理
def create
  @article = Article.new(article_params)
  if @article.save
    render json: @article, status: :created
  else
    render json: @article.errors, status: :unprocessable_entity
  end
end

実際のエラーレスポンス例

バリデーションエラー時のレスポンス

リクエスト

{
  "article": {
    "title": "",
    "content": "短い"
  }
}

レスポンス(ステータス422)

{
  "title": ["can't be blank"],
  "content": ["is too short (minimum is 10 characters)"]
}

テストで確認すべきポイント

✅ 必ずチェックすべきこと

  1. データベースの変化:正常系では+1、異常系では変化なし
  2. HTTPステータス:201(成功)or 422(エラー)
  3. レスポンスの内容:正しいデータが返ってくるか

🔍 さらに詳しくチェックしたいこと

  1. セキュリティ:不正なパラメータが弾かれるか
  2. エラーメッセージ:ユーザーに分かりやすいか
  3. パフォーマンス:処理時間は適切か

実践的なテストパターン

パターン1:複数のバリデーションエラー

it 'すべてのバリデーションエラーが返される' do
  post "/articles", params: {
    article: {
      title: "",        # 必須項目エラー
      content: "短い"    # 文字数エラー
    }
  }
  
  json = JSON.parse(response.body)
  
  # titleのエラーをチェック
  expect(json['title']).to include("can't be blank")
  
  # contentのエラーをチェック(複数エラーが同時に発生)
  expect(json['content']).to include("is too short")
end

パターン2:不正なパラメータの除外

it '許可されていないパラメータは無視される' do
  expect {
    post "/articles", params: {
      article: {
        title: "正常な記事",
        content: "この記事は正常な内容です。とても長い内容でバリデーションもクリアします。",
        admin: true,      # 不正なパラメータ
        secret: "hack"    # 不正なパラメータ
      }
    }
  }.to change(Article, :count).by(1)

  # 記事は作成されるが、不正なパラメータは保存されない
  article = Article.last
  expect(article.title).to eq("正常な記事")
  expect(article).not_to respond_to(:admin)    # adminというメソッドは存在しない
  expect(article).not_to respond_to(:secret)   # secretというメソッドも存在しない
end

パターン3:作成されたデータの詳細チェック

it '作成された記事の詳細情報が正しい' do
  travel_to Time.zone.parse('2024-01-01 12:00:00') do  # 時間を固定
    post "/articles", params: {
      article: {
        title: "テスト記事",
        content: "これはテスト用の記事内容です。十分な長さがあります。"
      }
    }

    json = JSON.parse(response.body)
    
    # 基本情報のチェック
    expect(json['title']).to eq('テスト記事')
    expect(json['content']).to eq('これはテスト用の記事内容です。十分な長さがあります。')
    
    # 自動で設定される項目のチェック
    expect(json['id']).to be_present           # IDが自動で設定される
    expect(json['created_at']).to be_present   # 作成日時が自動で設定される
    expect(json['updated_at']).to be_present   # 更新日時が自動で設定される
  end
end

エラーハンドリングの重要性

なぜエラーテストが必要?

実際のWebサイトでは、ユーザーがこんな操作をする可能性があります:

  • 📝 タイトルを空欄にして送信
  • 📝 極端に短い内容で送信
  • 🔧 ブラウザの開発者ツールで不正なデータを送信
  • 🌐 ネットワークエラーで中途半端なデータが送信

これらすべてに適切に対応できるかをテストで確認しましょう。

エラーレスポンスの例

# 実際に返されるエラーレスポンス
{
  "title": ["can't be blank"],
  "content": [
    "can't be blank",
    "is too short (minimum is 10 characters)"
  ]
}

このエラーレスポンスを見れば:

  • フロントエンド側で「タイトルを入力してください」と表示できる
  • 「内容は10文字以上入力してください」と具体的に案内できる

まとめ

POSTリクエストのテストでは、2つのシナリオを必ずテストしましょう:

🟢 正常系テスト

  • ✅ データが正しく作成される
  • ✅ 適切なステータスコード(201)が返る
  • ✅ 作成されたデータの内容が正しい

🔴 異常系テスト

  • ✅ 無効なデータでは作成されない
  • ✅ 適切なエラーステータス(422)が返る
  • ✅ 分かりやすいエラーメッセージが返る

覚えておきたいポイント

  1. change matcherexpect { }.to change(Model, :count).by(1)
  2. HTTPステータス:成功時201、エラー時422
  3. Strong Parameters:セキュリティのために必須
  4. JSON解析JSON.parse(response.body)でレスポンスを確認

POSTリクエストのテストをマスターすれば、ユーザー登録、商品注文、コメント投稿など、あらゆる「作成」機能のテストが書けるようになります!

次は、PATCHリクエスト(更新処理)のテストも学んでいきましょう 🚀

Happy Testing! 🎉

Discussion