Active AdminのControllerに手を加えた際、テストコードがほしくなりました。
Wikiを見たところ、controller specでの実装でした。
Testing your ActiveAdmin controllers with RSpec · activeadmin/activeadmin Wiki
ただ、現在では controller spec よりも request spec が推奨されています。
Rails: Rails 5 のサポート | RSpec 3.5 がリリースされました!
そこで、Active Admin向けのテストコードを request spec で書いてみました。
目次
- 環境
- プロダクションコードの準備
- テストコードの準備
- 正常系の request spec
- nameを入力しない場合の request spec
- 外部APIの呼び出しで例外が発生した場合の request spec
- ソースコード
環境
- Ruby 3.0.1
- Rails 6.1.4
- Active Admin 2.9.0
- Devise 4.8.0
- Active Adminの管理ページをログイン必須にするために使用
- rspec-rails 5.0.1
プロダクションコードの準備
前回の記事のコードを一部修正してプロダクションコードとします。
Modelにバリデーションを追加
name
を入力必須にします。
# app/models/fruit.rb class Fruit < ApplicationRecord validates :name, presence: true end
Active Adminで Model を作成する時にトランザクションを追加
オーバーライドしていた controllerの create
メソッドを変更し、
を追加します。
なお、flashについては、
- renderのときは
flash.now
- リダイレクトのときは
flash
を使います。
5.2 Flash | Action Controller の概要 - Railsガイド
ActiveAdmin.register Fruit do permit_params :name, :color controller do def create ApplicationRecord.transaction do super do |format| if @fruit.valid? call_api_with_params(permitted_params[:fruit][:name]) # redirectするので flash flash[:notice] = 'success' else # バリデーションエラー時はrenderされるので flash.now flash.now[:alert] = 'wrong!' end end end rescue StandardError flash.now[:error] = 'exception!' render :new end private def call_api_with_params(name) logger.info("======> call api with #{name}") end end
画面での動作確認
正常
バリデーションエラー
APIエラー
今のプロダクションコードでは発生し得ないので、メソッド call_api_with_params
で例外が出るように修正した時の表示となります。
テストコードの準備
プロダクションコードができたので、次は request spec を書きます。まずは準備です。
rspecまわり
Gemfileに
を追加し、bundle installします。
group :development, :test do gem 'factory_bot_rails' gem 'rspec-rails' end
続いて、rspecの初期設定とrequest specの雛形を生成します。
% bin/rails generate rspec:install Running via Spring preloader in process 37340 create .rspec exist spec create spec/spec_helper.rb create spec/rails_helper.rb % bin/rails generate rspec:request fruit Running via Spring preloader in process 37150 create spec/requests/fruits_spec.rb
rexmlの追加
rspecを実行したところ、以下のエラーが発生しました。
% bin/rails spec spec/requests/admin ... An error occurred while loading ./spec/requests/admin/fruits_spec.rb. - Did you mean? rspec ./spec/factories/admin_user.rb Failure/Error: require File.expand_path('../config/environment', __dir__) LoadError: cannot load such file -- rexml/document
原因は、Ruby3.0.1 の場合、rexml gemが不足しているためでした。
Rails 6.1, Ruby 3.0.0: tests error as they cannot load rexml - Stack Overflow
そこで、 rexml
もGemfileに追加してインストールします。
gem 'rexml'
factory_botによる admin user 作成
今回のActive AdminはDeviseによる認証を行っています。
そのため、テストコード中も admin user でログインする必要があります。
そこで、factory_bot を使って admin userを作成できるようにします。
まずは、spec/rails_helper.rb
に FactoryBot::Syntax::Methods
を追加します。
Configure your test suite | Setup | factory_bot/GETTING_STARTED.md at master · thoughtbot/factory_bot
config.include FactoryBot::Syntax::Methods
続いて、生成する admin user の設定を行います。
admin_userのfactoryは、 spec/factories/admin_user.rb
に作成します。
複数のadmin userをfactory_botで生成しても問題が起こらないよう、メールアドレスはシーケンスにします。
https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#inline-sequences
また、Deviseで生成した admin userは、パスワード項目として password
と password_confirmation
の2つが必要になるため、それぞれ設定します。
How To: Test controllers with Rails (and RSpec) · heartcombo/devise Wiki)
FactoryBot.define do factory :admin_user do sequence(:email) { |n| "person#{n}@example.com" } password { 'password' } password_confirmation { 'password' } end end
テストコード中で sign_in できるようにする
テストコード中でのログインを容易にするため、spec/rails_helper.rb
にDeviseの Devise::Test::IntegrationHelpers
を追加します。
heartcombo/devise: Flexible authentication solution for Rails with Warden.
config.include Devise::Test::IntegrationHelpers
以上で準備ができました。
正常系の request spec
spec/requests/admin/fruit_spec.rb
に作成します。
テストコードでは
- 各テストの実行前にログインすること
- 作成するときのpathは複数形(admin_fruits_path)、作成後にリダイレクトする先のpathは単数形(admin_fruits_path)
- 作成後にリダイレクトする先のpathを複数形にしてしまうと、
localhost:3000/admin/fruit.1
のようなURLになってしまう bin/rails routes
でも確認できる
- 作成後にリダイレクトする先のpathを複数形にしてしまうと、
- リダイレクト後の画面を確認する場合は、
follow_redirect!
を使ってリダイレクトに追随すること
あたりを頭に置いて実装します。
require 'rails_helper' RSpec.describe 'Admin::Fruits', type: :request do let(:admin_user) { create(:admin_user) } before do sign_in admin_user end describe '#create' do let(:name) { 'りんご' } let(:color) { '#000000' } let(:params) { { fruit: { name: name, color: color } } } context '登録に成功した場合' do before { post admin_fruits_path, params: params } # pathは複数形 it 'Fruitが登録されていること' do fruit = Fruit.find_by(name: name) expect(fruit).not_to eq nil expect(fruit.color).to eq nil expect(fruit.start_of_sales).to eq nil end it '作成したFruitの詳細画面へリダイレクトしていること' do expect(response).to have_http_status '302' fruit = Fruit.find_by(name: name) expect(response).to redirect_to(admin_fruit_path(fruit)) # pathは単数形 end it 'リダイレクト先の画面にflashが表示されていること' do follow_redirect! expect(response.body).to include 'success' end end # ...
nameを入力しない場合の request spec
こちらも同様な形で検証します。
なお、contextの中で name
を上書きしているため、このcontextの中では name
の値が nil
になっています。
context 'nameが未入力でエラーの場合' do # describeで定義した name を上書き let(:name) { nil } before { post admin_fruits_path, params: params } it 'Fruitが登録されていないこと' do expect(Fruit.find_by(name: name)).to eq nil end it '作成したFruitの登録画面のままであること' do expect(response).to have_http_status '200' end it 'エラーが表示されていること' do expect(response.body).to include 'be blank' end it 'エラーのflashが表示されていること' do expect(response.body).to include 'wrong!' end end
外部APIの呼び出しで例外が発生した場合の request spec
現在のプロダクションコードでは、外部APIの呼び出し時には例外が発生しません。
そこで、外部APIを呼び出しているメソッド call_api_with_params
で例外が発生するよう、メソッドを差し替えます。
また、Active AdminのControllerは、デフォルトでは Admin::<Model名>Controller
という名前になるため、今回は Admin::FruitsController
に対して差し替えを行います。
なお、request specではControllerのインスタンスをどのように差し替えるのが適切か分からなかったため、 expect_any_instance_of
でControllerのどのインスタンスでも例外が発生するようにしています。
もし、より良い方法をご存じの方がいれば、教えていただけるとありがたいです。
context 'nameを含むリクエストを送ったものの、APIでエラーになった場合' do before do expect_any_instance_of(Admin::FruitsController).to receive(:call_api_with_params) .with(name) .once .and_raise(StandardError) post admin_fruits_path, params: params end it 'Fruitが登録されていないこと' do expect(Fruit.find_by(name: name)).to eq nil end it '作成したFruitの登録画面のままであること' do expect(response).to have_http_status '200' end it 'エラーのflashが表示されていること' do expect(response.body).to include 'exception!' end end
以上のように、Active Admin向けのテストコードを request spec で書けることが分かりました。
ソースコード
Githubに上げました。
https://github.com/thinkAmi-sandbox/rails_with_active_admin_app
関係するプルリクはこちらです。
https://github.com/thinkAmi-sandbox/rails_with_active_admin_app/pull/3