0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails API】RSpecでModelSpec・RequestSpecを書きながら学んだことまとめ

0
Posted at

はじめに

RSpecのテストコードを書いて、大体の書き方や考え方などわかってきたので、その備忘録としてまとめます。

前提

  • Docker
  • Rails API
  • 実行コマンド:docker compose exec back bash rspec

基本のテストコード

複雑な実装でなければ下記のようなテストコードでも要件を満たすと考えています。

model spec

モデルに定義したバリデーションや振る舞いが期待通りか確認します。

例えば、次のカラムだった時を考えます。

db/schema.rb
create_table "categories", force: :cascade do |t|
  t.datetime "created_at", null: false
  t.string "name", null: false
  t.datetime "updated_at", null: false
end

主に、nameに対して、空でないこと、重複がないことをバリデーションとします。

models
class Category < ApplicationRecord
  validates :name, presence: true, uniqueness: true
end

FactoryBotを使ってテストデータを用意します。

spec/factories/category.rb
FactoryBot.define do
  factory :category do
    sequence(:name) { |n| "category_#{n}"  }
  end
end

sequenceを使うと重複したデータが作成されないようにできます。

そして下記のようにテストコードをまとめました。

spec/models/category_spec.rb
require 'rails_helper'

RSpec.describe Category, type: :model do
  describe 'validations' do
    context 'when all attributes are valid' do
      let(:category) { build(:category) }

      it 'is valid' do
        expect(category).to be_valid
      end
    end

    describe 'presence validation' do
      context 'when name is blank' do
        let(:category) { build(:category, name: nil) }

        it 'is invalid' do
          expect(category).to be_invalid
        end
      end
    end

    describe 'uniqueness validation' do
      context 'when name already exists' do
        let!(:category_before) { create(:category, name: "重複保存不可") }
        let(:category) { build(:category, name: "重複保存不可") }

        it 'is invalid' do
          expect(category).to be_invalid
        end
      end
    end
  end
end

バリデーションに合わせて、createとbuildを使い分けると良いと思いました。

  • buildはDBへ保存しないため、presenceなどDBを必要としないバリデーションの確認に便利
  • createはDBへ保存するため、uniquenessなどDBに保存されたデータとの比較が必要な場合に使う

requests spec

データを作成して、想定したステータスやjsonが返ってくるか判断します。

次のようなコントローラーで考えます。

app/controller/api/v1/health_controller.rb
class Api::V1::HealthController < ApplicationController
  def index
    render json: { status: "success" }, status: :ok
  end
end

APIのレスポンスやステータス、データの取得・更新が期待通りか確認します。

spec/requests/api/v1/health_spec.rb
require 'rails_helper'

RSpec.describe 'Api::V1::Health', type: :request do
  describe 'GET /api/v1/health' do
    it 'returns status 200' do
      get '/api/v1/health'

      expect(response).to have_http_status(:ok)
      
      json = response.parsed_body
      
      expect(json).to include(
        "status"
      )
    end
  end
end

認証周りのテストについて

認証にはAuth、セッション、JWTなどさまざまな方法があります。
通常に加えて、Google認証などあるため、しっかりテストを書こうとすると複雑になります。

例えば、次のようなJWT認証ロジックがあるとします。長くなるため省略しています。

app/controller/concerns/authenticatable.rb
module Authenticatable
  extend ActiveSupport::Concern

  included do
    before_action :authenticatable!
    attr_reader :current_user
  end

  def authenticatable!
    token = request.authorization&.split(" ")&.last

    ...省略

認証処理そのものではなく、認証後のAPIの挙動を確認したかったため、stubを使って認証済み状態を再現しました。

spec/support/authentication_helper.rb
module AuthenticationHelper
  def stub_authentication(user)
    allow_any_instance_of(ApplicationController)
      .to receive(:authenticatable!) do |controller|
        controller.instance_variable_set(:@current_user, user)
      end
  end
end

リクエストスペックを次のようにまとめました。

spec/requests/api/v1/categories_spec.rb
require 'rails_helper'

RSpec.describe 'Api::V1::Categories', type: :request do
  describe 'GET /api/v1/categories' do
    let!(:user) { create(:user) }
    let!(:category) { create(:category) }
    let!(:question) { create(:question, category: category) }
    let!(:choice) { create(:choice, question: question) }

    context 'when authenticated' do
      let(:headers) { { CONTENT_TYPE: 'application/json', Authorization: 'Bearer fake_token' } }

      before do
        stub_authentication(user)
      end

      it 'returns all categories with status 200' do
        get '/api/v1/categories', headers: headers

        expect(response).to have_http_status(:ok)

        json = response.parsed_body

        expect(json.first).to include(
          'id',
          'name',
          'questions'
        )

        expect(json.first['questions'].first).to include(
          'id',
          'content',
          'choices'
        )
      end
    end
  end
end

認証で重要なところは下記です。

let!(:user) { create(:user) }
let(:headers) { { CONTENT_TYPE: 'application/json', Authorization: 'Bearer fake_token' } }

before do
  stub_authentication(user)
end

it 'returns all categories with status 200' do
    get '/api/v1/categories', headers: headers

    ...省略
  • userを作成してstub_authentication(user)へ渡す
  • Authorizationヘッダーを付与して認証済みリクエストとして送る

共通項:エラーが起きたらデバッグ

正しくデータが作られないとしたら、バリデーションがどうなっているか確認します。
想定通りにメソッドへデータが送られないとしたら、データの型や構成、リクエストが正しいか確認します。

binding.pryを活用して、想定したデータが返ってきているか、レスポンスのJSON構造がどうなっているかなどを確認します。

おわりに

テストコードを書くのはめんどくさいし、最初は見て意味がわからないです。
しかし、慣れてくるとテストコードを書くのもだんだん作業になってくると感じました。

テストコードは

  • テーブル設計
  • JSONの構造
  • create / build の違い
  • FactoryBot
  • stub
  • アプリケーションのロジック

などを理解していれば、AIでもある程度生成できます。むしろAIを使ったほうが効率よくテストコードを書けると考えています。

そう考えたとき、テストコードを書く技術よりも「何を検証したいのか」を考えることが大切だと感じました。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?