概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Speed up and refactor your RSpec tests in 4 simple steps
- 原文公開日: 2018/03/25
- 著者: Paweł Dąbrowsk
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_truthy
やbe_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 :user
やFactoryBot.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.rb
にrequire '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%高速化することもあります。データベースのレコードが不要な場合は常にスタブで塞いでおく方法もあります。