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
circleciで実行
githubとcircleciの画面からポチポチ連携するだけなので省略
ダメだ。。力尽きた。。以上です
参考)
[Rails] rspec + factory_bot + database_cleaner で、APIのテストを書く - Qiita
Rails5へのRspec導入から実行確認まで - Qiita