Rails + Active Adminで、Active Admin向けのテストコードを request spec で書いてみた

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 で書いてみました。

 
目次

 

環境

  • 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.rbFactoryBot::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は、パスワード項目として passwordpassword_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 に作成します。

テストコードでは

あたりを頭に置いて実装します。

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