Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(RequestSpec編)

# はじめに

本記事はAPIをRailsのAPIモードで開発し、フロント側をVue.js 3で開発して、認証基盤にdevise_token_authを用いてトークンベースの認証機能付きのSPAを作るチュートリアルのRequestSpec編の記事になります。(今回はVue.jsには触れませんのであしからず)

前回: [Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(ファイル投稿編)](https://qiita.com/kk-icare/items/50d440b3994234cb76db)

次回: [Rails APIモード + devise_token_auth + Vue.js 3 で認証機能付きのSPAを作る(2要素認証設定編)](https://qiita.com/kk-icare/items/9bd25f906865ce6578bc)

# Rspecの導入

今回はmini-testではなくRspecを用いてテストを行います。理由は筆者がRspecの方が好きだからです(mini-testはあまり触ったことがない、、、)

## Gemfileの編集

Gemfileに以下を追加して、bundle installを実行してください。

```ruby
# Gemfile

group :development, :test do
  gem "rspec-rails"
  gem "factory_bot_rails"
  gem 'faker' # ダミーデータを作成するため
  gem 'gimei' # 日本語のダミーデータを作成するため
end

# Terminal

$ bundle
```

## Generatorの実行

その後、Rspecを導入する以下のコマンドを実行してください。

```terminal
$ rails g rspec:install

create  .rspec
create  spec
create  spec/spec_helper.rb
create  spec/rails_helper.rb
```

.rspecファイルの中身を以下のように修正してください。

```
--require spec_helper
--format documentation
--color
```

--format documentationはRspecの実行結果を見やすく加工するため、--colorは実行結果を色つきにして見やすくするために追記しています。

## application.rbの編集

```rails g model```や```rails g scaffold```を実行した時に余計なファイルが作成されないように、generator実行時の挙動を修正します。

config/application.rbを以下のように修正してください。

```ruby
# config/application.rb

module App
  class Application < Rails::Application

    # 省略

    # ここから追加
    config.generators do |g|
      g.test_framework :rspec,
            view_specs: false,
            helper_specs: false,
            controller_specs: false,
            routing_specs: false
    end
    # ここまで
  end
end
```

## rails_helper.rbの修正

FactoryBotの省略記法を使うために、以下の行を追加してください。

```ruby
RSpec.configure do |config|
  # 省略

  # 以下を追加
  config.include FactoryBot::Syntax::Methods

  # 省略
end
```

これで、```Factorybot.create(:user)```ではなく、```create(:user)```のように書くことができます。

# ログインのヘルパーモジュールを作成

RequestSpecの中でログイン状態を表現したり、getやpostメソッド等のHTTPリクエストメソッドをオーバーライドするヘルパーモジュールを定義します。

以下の実装方法を参考にしました。

https://gist.github.com/blaze182/3a59a6af8c6a7aaff7bf5f8078a5f2b6

以下のコマンドを実行してください。

```terminal
$ mkdir spec/supports/

$ touch spec/supports/auth_helper.rb
```

作成したファイルを以下のように修正します。

```ruby
module AuthHelper
  HTTP_HELPERS_TO_OVERRIDE = [:get, :post, :patch, :put, :delete]

  HTTP_HELPERS_TO_OVERRIDE.each do |helper|
    define_method(helper) do |path, **args|
      add_auth_headers(args)
      args == {} ? super(path) : super(path, **args)
    end
  end

  def login(user)
    @user = user
    @auth_token = @user.create_new_auth_token
  end

  def logout
    @user = nil
    @auth_token = nil
  end

  private

  def add_auth_headers(args)
    return unless defined? @auth_token
    args[:headers] ||= {}
    args[:headers].merge!(@auth_token)
  end
end
```

修正できたらrails_helper.rbでこのファイルをincludeします。

```ruby
RSpec.configure do |config|
  # 省略

  # ここから追加
  Dir[Rails.root.join('spec/supports/**/*.rb')].each { |f| require f }
  config.include AuthHelper, type: :request
  # ここまで

  # 省略
end
```

これでRequestSpecの中で、オーバーライドされたgetメソッドやpostメソッド、login、logoutメソッドを実行することができます。

# RequestSpecの作成

投稿APIのRequestSpecを作成します。

以下のコマンドを実行してください。

```terminal
$ mkdir spec/requests

$ mkdir spec/factories

$ touch spec/requests/posts_spec.rb

$ touch spec/factories/posts.rb

$ touch spec/factories/users.rb
```

まずはFactoryファイルから編集しましょう。

```ruby
# spec/factories/users.rb

FactoryBot.define do
  factory :user do
    provider                    { 'email' }
    uid                         { Faker::Internet.safe_email }
    password                    { Faker::Internet.password }
    email                       { uid }
    name                        { Gimei.name }
  end
end
```

```ruby
# spec/factories/posts.rb

FactoryBot.define do
  factory :post do
    title { Faker::Lorem.word }
    body  { Faker::Lorem.sentence }

    association :user
  end
end
```

これでspecの中で```create(:user)```等が実行できます。

次にposts_spec.rbを書いていきます。

```ruby
require 'rails_helper'

RSpec.describe "Posts API", type: :request do
  describe "ログイン済" do
    describe "GET /posts" do
      it "投稿の一覧を取得できること" do
        user = create(:user)
        post = create(:post, user: user)
        login user

        get "/posts"

        expect(response).to have_http_status(:success)
        post_json = JSON.parse(response.body).first
        expect(post_json["id"]).to eq post.id
        expect(post_json["title"]).to eq post.title
        expect(post_json["body"]).to eq post.body
        expect(post_json["created_at"]).to eq post.created_at.iso8601(3)
        expect(post_json["user_name"]).to eq post.user.name
      end
    end
    describe "GET /posts/:id" do
      it "投稿の詳細を取得できること" do
        user = create(:user)
        post = create(:post, user: user)
        login user

        get "/posts/#{post.id}"

        expect(response).to have_http_status(:success)
        post_json = JSON.parse(response.body)
        expect(post_json["id"]).to eq post.id
        expect(post_json["title"]).to eq post.title
        expect(post_json["body"]).to eq post.body
        expect(post_json["created_at"]).to eq post.created_at.iso8601(3)
        expect(post_json["user_name"]).to eq post.user.name
      end
    end
    describe "POST /posts" do
      it "投稿を作成できること" do
        user = create(:user)
        login user
        params = attributes_for :post

        post "/posts", params: params

        expect(response).to have_http_status(:success)
        post_json = JSON.parse(response.body)
        post = Post.find(post_json["id"])
        expect(post_json["id"]).to eq post.id
        expect(post_json["title"]).to eq post.title
        expect(post_json["body"]).to eq post.body
        expect(post_json["created_at"]).to eq post.created_at.iso8601(3)
        expect(post_json["user_name"]).to eq post.user.name
      end
    end
    describe "PATCH /posts/:id" do
      it "投稿を更新できること" do
        user = create(:user)
        post = create(:post, user: user)
        login user
        params = attributes_for :post

        patch "/posts/#{post.id}", params: params

        expect(response).to have_http_status(:success)
        post_json = JSON.parse(response.body)
        _post = Post.find(post_json["id"])
        expect(post_json["id"]).to eq _post.id
        expect(post_json["title"]).to eq _post.title
        expect(post_json["body"]).to eq _post.body
        expect(post_json["created_at"]).to eq _post.created_at.iso8601(3)
        expect(post_json["user_name"]).to eq _post.user.name
      end
    end
  end

  describe "未ログイン" do
    it "投稿一覧にアクセスができないこと" do
      user = create(:user)
      post = create(:post, user: user)

      get "/posts"

      expect(response.status).to eq 401
    end
  end
end
```

authenticate_user!をbefore_actionでコールしているため、ログイン済と未ログイン済のテストケースを用意しています。

ログイン済の場合は、各種APIを叩いたあとに返却される返り値が正しいかどうか、
未ログインの場合は401エラーが返却されるかどうかをテストしています。

# まとめ

最低限APIのテストを行うなら上記の実装で十分かと思います。

次回はユーザー登録機能を実装する予定です。