rails5 rspecでテスト書きながらapi開発、circleciも使ってciぽいことしてみる

はじめに

今回やりたいことはざっくり以下
・rails5でrest api実装
・devise_token_authを使って認証機能を実装
rspecでテストコード実装
・circleciで動かす

それぞれ少ししか触れないがなんとなくciぽい雰囲気がわかるように手順メモしておく

事前準備

なにはともあれrailsプロジェクト作成
今回のテストはrspecで書くので--skip-test-unitオプションを指定してtestディレクトリを作らないようにする

$ rails new spec_test --api --skip-test-unit


db作成。ローカルで動かすだけなのでとりあえずsqlite

$ bundle exec rake db:create

ここまでできたらとりあえず動かしてみる。以下のコマンドでサーバー起動して「http://localhost:3000」にアクセスしてページが表示されればok

$ rails s

rspec

bundleインストールしてセットアップ。
factory_botとdatabase_cleanerはたぶん今後最低限使いそうなので一緒にいれておく
factory_botはテストデータの作成に、database_cleanerは登録したテストデータがゴミとして残らないように削除するために使う

$ vi Gemfile
gem 'rspec-rails'
gem 'factory_bot_rails'
gem 'database_cleaner'
・・・

$ bundle install --path=vendor/bundle
$ bundle exec rails g rspec:install

以下を記述しておけば「rails g controller」とかしたときにrailsが自動でrspecのひな形を作ってくれる。
今回はrequest specしか作らないので以下のようにした。

$ vi config/application.rb

・・・

config.generators do |g|
  g.test_framework :rspec,
                   fixtures: false,
                   view_specs: false,
                   helper_specs: false,
                   routing_specs: false,
                   controller_specs: false,
                   request_specs: true
  g.fixture_replacement :factory_bot, dir: "spec/factories"
end

adtabase_cleanerでrspec開始前にテーブルをクリア(truncate)。
各exampleごとにトランザクションを貼ってロールバックする形となる。
今回はexampleごとにテストデータを準備することにしたのでこの形。
参考)https://qiita.com/shoichiimamura/items/25942acc1d1bd78ef9c3

$ vi spec/spec_helper.rb

## 追加
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'

RSpec.configure do |config|

  ・・・

  ## 追加
  config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

以下はfactory_botを使うときのおきまりの記述ぽい、たぶん
テストデータを作るときに「FactoryBot::create」と書かないといけないところを「create」と書けるようになる

$ spec/rails_helper.rb
RSpec.configure do |config|

  ・・・

  config.include FactoryBot::Syntax::Methods
end

とりあえずrspecの設定は完了したので1度実行してみる。テストはないがエラーが出ないことを確認しておく

$ ./bin/bundle exec rspec --format documentation
No examples found.

Finished in 0.1503 seconds (files took 1.34 seconds to load)
0 examples, 0 failures

devise_token_auth

今回は以下を実装、テストすることとする
・メールアドレス+パスワードをパラメータでサインアップ
・メールアドレス+パスワードをパラメータでログイン
・ログイン済みでないとアクセスできないエンドポイントの実装
ログイン済みかどうかの判定方法はログインに成功したときにレスポンスヘッダーに認証情報が含まれるので以降のリクエストではその値をリクエストヘッダーに含める形となる


bundleインストール

$ vi Gemfile
gem 'jbuilder', '~> 2.5'
gem 'rack-cors'
gem 'devise'
gem 'devise_token_auth'
・・・

$ bundle install --path=vendor/bundle

以下のコマンドでuserモデルが認証の対象となり、userモデルが作られる

$ rails g devise_token_auth:install User auth

また、今回はsignup後にメールアドレスが存在するかのチェックまでは行わないので以下のようにconfirmableの部分をコメントアウトして無効とする

$ vi db/migrate/xxx_devise_token_auth_create_users.rb
・・・
      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

・・・

    # add_index :users, :confirmation_token,   unique: true

・・・

$ ./bin/rake db:migrate

実装

devise_token_authを使うと以下のapiが自前で実装しなくても作られるので今回はそれのテストコードを書く。
signin : POST /auth/sign_in
signup : POST /auth

もちろんsignin、signup以外の機能も作られるがいっぱいあるのでここでは扱わない

まずはFactoryBotで生成するテストデータはspec/factories以下に定義する

$ vi spec/factories/users.rb

FactoryBot.define do
  factory :user do
    sequence(:name) { |n| "test_#{n}" }
    sequence(:email) { |n| "test+#{n}@example.com" }
    password { 'password' }
  end
end

コードの中「create(:user)」 と記述すればこれを使って以下のようなテストデータを生成してくれる
nの部分は連番でcreateしたrspect内でcreateした回分インクリメントされていく

{ name: "test_0", email: "test+0@example.com", password: "password" }
signupのテスト(spec)

request specはrequests以下に実装していく。
コードみればなんとなくわかりそうだが、リクエストしてレスポンスが期待値どおりか検証するコードを書いていく
「let!(:user) {create(:user)}」で定義したタイミングでFatoryBotでuserのテストデータが1件作られる
なので、実際に/auth/sign_inにリクエストするときにDBに必ず存在するユーザーでログインのテストする形となる

requests/auth_spec.rb

require 'rails_helper'

RSpec.describe 'Auth', type: :request do

  let!(:user) {create(:user)}

  describe 'POST /auth/sign_in' do
    context '正常' do
      before do
        post '/auth/sign_in', params: {email:user['email'], password: 'password'}
      end

      example 'HTTPステータスが200であること' do
        expect(response.status).to eq(200)
      end

      example 'レスポンスが正しいこと' do
        json = JSON.parse(response.body)
        expect(json['data']['email']).to eq(user['email'])
      end
    end

    context '異常' do
      before do
        post '/auth/sign_in', params: {email:user['email'], password: 'passwordxxx'}
      end

      example 'HTTPステータスが401であること' do
        expect(response.status).to eq(401)
      end

      example 'レスポンスが正しいこと' do
        json = JSON.parse(response.body)
        expect(json['success']).to eq(false)
      end
    end
  end

end

テストを実行する

$ bundle exec rspec --format documentation spec/requests/auth_spec.rb
Auth
  POST /auth/sign_in
    正常
      HTTPステータスが200であること
      レスポンスが正しいこと
    異常
      HTTPステータスが401であること
      レスポンスが正しいこと

上記のようになり期待通りの動作であることが確認できた

signinのテスト(spec)

Signupと同じようにテストを書く

RSpec.describe 'Auth', type: :request do

  ・・・

  describe 'POST /auth' do
    context '正常' do
      let(:params) {{email:'test@example.com', password:'password'}}
      before do
        post '/auth', params: params
      end

      example 'HTTPステータスが200であること' do
        expect(response.status).to eq(200)
      end

      example 'レスポンスが正しいこと' do
        expect(JSON.parse(response.body)['status']).to eq("success")
      end
    end
  end
end

テストを実行する

$ bundle exec rspec --format documentation spec/requests/auth_spec.rb
Auth

  ・・・

  POST /auth
    正常
      HTTPステータスが200であること
      レスポンスが正しいこと
認証が必要なapi

devise_token_authでの認証処理は
ログインに成功するとレスポンスヘッダーにuid、client、access-token返す
認証が必要なapiへリクエストする際に取得したヘッダーをリクエストヘッダーに含めてリクエストする

認証に成功すればcurrent_userに認証に成功したuserモデルがセットされるのでログイン中かどうかの判定ができる
/usersにリクエストして認証済みなら本人の情報をかえすapiとする

controllers/users_controller.rb

class UsersController < ApplicationController
  def index
    if current_user.nil?
      render status: 401, json: {status: 401} if current_user.nil?
    else
      render json: current_user
    end
  end
end

config/routes.rb

Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', at: 'auth'
    get '/users', to: 'users#index'
end

続いてテストコード

/usersにリクエストする前にログインのapiへリクエスト後に取得したヘッダーを付与してリクエストするようにする

spec/requests/users_spec.rb

require 'rails_helper'

RSpec.describe 'Users', type: :request do

  let!(:user) {create(:user)}

  def auth_headers
    post '/auth/sign_in', params: {email:user['email'], password: 'password'}
    { 'uid'=>response.header['uid'], 'client'=>response.header['client'], 'access-token'=>response.header['access-token'] }
  end

  describe 'GET /users' do
    context '未ログインの場合' do
      example 'HTTPステータスが401であること' do
        get '/users'
        expect(response.status).to eq(401)
      end
    end
    context 'ログインずみの場合' do
      before do
        get '/users', headers: auth_headers
      end
      example 'HTTPステータスが200であること' do
        expect(response.status).to eq(200)
      end
      example 'レスポンスが正しいこと' do
        expect(JSON.parse(response.body)['id']).to eq(user['id'])
      end
    end
  end
end

テスト実行

$ bundle exec rspec --format documentation spec/requests/auth_spec.rb
・・・

結果書かないけどSuccessになるはず

circleci

あとはcircleciの設定すればgithubにプッシュするタイミングでテストを行ってくれる
circleciの挙動確認するために毎回プッシュしなくてもローカルにcircleciをインストールすれば動作確認可能なのでcircleciをインストールする

circleci localインストール

$ curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && chmod +x /usr/local/bin/circleci
circleci update

localでcircleci

.circleci/config.yml

version: 2
jobs:
  build:
    docker:
      - image: circleci/ruby:2.4.1-node-browsers
    working_directory: ~/repo
    steps:
      - checkout
      - restore_cache:
          key: bundle-{{ checksum "Gemfile.lock" }}
      - run:
          name: bundle install
          command: bundle install --path=vendor/bundle
      - save_cache:
          key: bundle-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle
      - run:
          echo -e "export RAILS_ENV=test" >> $BASH_ENV
      - run:
          name: test
          command: bundle exec rspec --format documentation

ローカルでcircleci実行

$ circleci build .circleci/config.yml
githubへ登録

.gitignoreにvendor/bundle以下を追加してremote urlセットしてコミットしてプッシュ

.gitignore

# bundle
vendor/bundle
circleciで実行

githubとcircleciの画面からポチポチ連携するだけなので省略

ダメだ。。力尽きた。。以上です

参考)
[Rails] rspec + factory_bot + database_cleaner で、APIのテストを書く - Qiita
Rails5へのRspec導入から実行確認まで - Qiita