Rails tips: RSpecテストの高速化/リファクタリングに役立つ4つの手法(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: RSpecテストの高速化/リファクタリングに役立つ4つの手法(翻訳)

どんなアプリであってもテストは非常に重要な部分ですし、テストをきれいに書くこともやはり重要です。しかしテストスイートを高速かつきれいに保つのが困難になることがあります。特に自分より前に(あるいは現在)複数の開発者が寄ってたかって作った案件でそうなりがちです。本記事では、主に2つの面でテストを改善することに注力します。テストで繰り返し出現している共通部分のリファクタリングと、データベースリクエスト削減によるテストの高速化です。準備はよろしいでしょうか?

1. shared_examples

次の例で考えてみましょう。

class AgePolicy
  def old_enough?(age)
    age >= 18
  end
end

このシンプルなポリシーは、指定のユーザーがアプリでしかるべき操作を行ってよい年齢になっているかどうかをチェックします。このクラスのごく簡単なテストがあります。

require 'spec_helper'

describe AgePolicy do
  describe '#old_enough?' do
    it 'ユーザーが16歳ならfalseを返す' do
      policy = AgePolicy.new

      expect(policy.old_enough?(16)).to eq(false)
    end

    it 'ユーザーが12歳ならfalseを返す' do
      policy = AgePolicy.new

      expect(policy.old_enough?(12)).to eq(false)
    end

    it 'ユーザーが18歳ならtrueを返す' do
      policy = AgePolicy.new

      expect(policy.old_enough?(18)).to eq(true)
    end

    it 'ユーザーが20歳ならtrueを返す' do
      policy = AgePolicy.new

      expect(policy.old_enough?(20)).to eq(true)
    end
  end
end

ご覧のとおり、テストで変動している部分は年齢しかありません。こんなときは、RSpecテストフレームワークのshared_examples機能が使えます。

require 'spec_helper'

describe AgePolicy do
  describe '#old_enough?' do
    shared_examples '操作を行ってよい年齢のユーザー' do |age|
      it "ユーザーが#{age}歳ならtrueを返す" do
        policy = AgePolicy.new

        expect(policy.old_enough?(age)).to eq(true)
      end
    end

    shared_examples '操作を行えない年齢のユーザー' do |age|
      it "ユーザーが#{age}歳ならfalseを返す" do
        policy = AgePolicy.new

        expect(policy.old_enough?(age)).to eq(false)
      end
    end

    it_behaves_like '操作を行えない年齢のユーザー', 16
    it_behaves_like '操作を行えない年齢のユーザー', 12
    it_behaves_like '操作を行ってよい年齢のユーザー', 18
    it_behaves_like '操作を行ってよい年齢のユーザー', 20
  end
end

これでテストは少し速くなりましたが、まだ終わりではありません。テストは少しばかり読みにくくなったものの、同じコードをexampleごとに繰り返さなくなりました。

2. カスタムマッチャー

RSpecにはさまざまな便利マッチャーがあります。上の例で既にbe_truthybe_falseyを使いました。指定の値を期待するのに同じコードを何度も何度も繰り返していることがあります。コントローラでレスポンスをテストする場合が手頃なサンプルだと思います。次の例を考えてみましょう。

class SomeController
  def show
    render json: { success: true }
  end
end

レスポンスのsuccess属性がtrueになるかどうかをどのようにテストすればよいでしょうか?次のようにします。

describe SomeController do
  describe 'GET #show' do
    it 'successレスポンスを返す' do
      get :show, id: 11, format: :json

      expect(JSON.parse(response.body)).to eq({success: true})
    end
  end
end

上のテストを以下のように書ければこんな簡単な話はありません。

expect(response).to be_json_success

この行を見ただけではマッチャーが何をしているのかよくわかりませんが、アプリ全体に一貫したロジックがあるなら、このようなマッチャーを自作すれば挙動はおのずと分かるでしょう。こういうマッチャーを作成するには、spec/supportディレクトリの下にmatchers.rbファイルを作成し、次のようにマッチャーの定義をそこに書かなければなりません(ファイル名は何でも構いません)。

RSpec::Matchers.define :be_json_success do |expected|
  match do |actual|
    json_response = JSON.parse(actual.body)
    expect(json_response['success']).to eq(true)
  end
end

最後にspec_helper.rbファイルにrequire 'support/matchers'を記述してマッチャーをrequireすれば完了です。

3. ファクトリーから不要な関連付けを取り除く

このメソッドは、アプリでFactoryBot gemを使ってデータベース内の実際のレコードを扱っている場合に便利です。たとえばUserモデルがあり、各ユーザーは1つのContactレコードと1つのLocationレコードを持っているとします。このときのファクトリーは次のようになるでしょう。

FactoryBot.define do
  factory :user do
    contact
    location
  end
end

FactoryBot.create :userFactoryBot.build :userを使うたびに、追加のレコードが2つ作成されます。もちろんbuildでもです。関連付けられたレコードを作成したくない場合は、代わりにFactoryBot.build_stubbed :userを使うことをおすすめします。

たとえばテスト全体で使われる基本のファクトリーがあるとします。2つの関連付けが常にデータベースで必要なら構いませんが、そうでなければテストのパフォーマンスを一気に向上させるチャンスです。基本のファクトリーが常にこれらの属性を持たなければならないのは、バリデーションにパスする必要がある場合に限られます。関連付けがときどき必要になるのであれば、traitsの利用をご検討ください。

FactoryBot.define do
  factory :user do
    first_name { "John" }
    last_name { "Doe" }

    trait :with_location do
      location
    end
  end
end

後はFactoryBot.create :user, :with_locationでファクトリーを呼べばよいのです。

4. Railsのトランザクションテスト機能を活用する

データベースの1レコードだけあればよいという状況は、テスト全体でどのぐらい占めていますか?先のuserファクトリーで簡単なテストを書いてみましょう。

require 'spec_helper'

describe User do
  let!(:user) { FactoryBot.create :user }

  it '何かする' do
    # userのテスト
  end

  it '何かする' do
    # userのテスト
  end

  it '何かする' do
    # userのテスト
  end
end

上のテストではUserのレコードがいくつ作成されるでしょうか。exampleごとに1回なので答えは合計3回です。test-prof gemの素晴らしい機能であるlet_it_beを使えばこれを回避できます。このヘルパーはRailsのトランザクションテスト機能を利用してレコードを最初に1回だけ作成し、テストが終了したら削除します。gemをインストールしたら、spec_helper.rbrequire 'test_prof/recipes/rspec/let_it_be'と記述すればこのヘルパーを使えるようになります。

require 'spec_helper'

describe User do
  let_it_be(:user) { FactoryBot.create :user }

  it '何かする' do
    # userのテスト
  end

  it '何かする' do
    # userのテスト
  end

  it '何かする' do
    # userのテスト
  end
end

以上でおしまいです。同じレコードを使うexampleが多数ある場合は、specが50%高速化することもあります。データベースのレコードが不要な場合は常にスタブで塞いでおく方法もあります。

関連記事

Rails tips: RSpecテストを遅くする悪い書き方3種(翻訳)

Rails tips: RSpecのテスト設計でよくあるやらかし4種(翻訳)

Rails: テストのリファクタリングでアプリ設計を改良する(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ