概要
モデル間のhas_many、has_one、belongs_toをテストするためのSpecを自動生成して実行させます。ERBでコード生成してあれこれする感じです。
環境
Ruby: 2.3.1
Rails: 5.0
目次
- モデル間のリレーションSpecについて
- 自動生成させるモジュールを作る
- 実際に自動実行させる
モデル間のリレーションSpecについて
RSpecにはモデル間のリレーションをテストするためのマッチャーが用意されています。
例えば、Customer
モデルとOrder
モデルがあるとします。Order
はcustomer_id
を保持しており、一対多の関係です。
class Customer < ApplicationRecord
has_many :orders, dependent: :destroy
end
class Order < ApplicationRecord
belongs_to :customer
end
これらモデル間のリレーションSpecは、例えば以下のように書くことができます。
RSpec.describe Customer, type: :model do
describe 'association' do
describe 'has_many' do
it { is_expected.to have_many(:orders).dependent(:destroy) }
end
end
end
RSpec.describe Order, type: :model do
describe 'association' do
describe 'belongs_to' do
it { is_expected.to belong_to(:customer) }
end
end
end
このSpecを書いておくことによって、例えば、has_many
は定義したのにbelongs_to
を定義し忘れたとか、タイピングミスとかを防ぐことができます。
自動生成させるモジュールを作る
このようなSpecをモデルごとに書いていたのですが、**これって自動生成できるんじゃない?**と、思い返りました。
具体的な実現方法としては、erbで上記テストコードを自動生成し、モデルのSpec実行時にコードを挿入して実行させる、という感じです。
今回は、実行するためのモジュールをlib
配下に作りました。
require 'erb'
module AutoSpec
module ModelSpec
def exe_relation_specs
@klass = Object.const_get(to_s.split('::').last.split('_').first)
temp = open("#{File.expand_path('..', __FILE__)}/templates/relations.erb") do |f|
ERB.new(f.read).result(binding)
end
instance_eval temp
end
end
end
<% if @klass.reflect_on_all_associations %>describe 'relation' do
<% if (ass = @klass.reflect_on_all_associations(:has_one)).present? %>context 'has_one' do
<% ass.each do |as| %>
it { is_expected.to have_one(:<%= as.name %>)<%= as.options.reduce([]) { |a, e| a << (%w(foreign_key primary_key).include?(e[0].to_s) ? "\.with_#{e[0]}(:#{e[1]})" : "\.#{e[0]}(:#{e[1]})") }.join %> }<% end %>
end<% end %>
<% if (ass = @klass.reflect_on_all_associations(:has_many)).present? %>context 'has_many' do
<% ass.each do |as| %>
it { is_expected.to have_many(:<%= as.name %>)<%= as.options.reduce([]) { |a, e| a << (%w(foreign_key primary_key).include?(e[0].to_s) ? "\.with_#{e[0]}(:#{e[1]})" : "\.#{e[0]}(:#{e[1]})") }.join %> }<% end %>
end<% end %>
<% if (ass = @klass.reflect_on_all_associations(:belongs_to)).present? %>context 'belongs_to' do
<% ass.each do |as| %>
it { is_expected.to belong_to(:<%= as.name %>)<%= as.options.reduce([]) { |a, e| a << (%w(foreign_key primary_key).include?(e[0].to_s) ? "\.with_#{e[0]}(:#{e[1]})" : "\.#{e[0]}(:#{e[1]})") }.join %> }<% end %>
end<% end %>
end<% end %>
せっかくなのでshared_contextを定義して、その中で実行させるようにします。
shared_context :relation_specs, relation_specs: true do
extend AutoSpec::ModelSpec
exe_relation_specs
end
実際に自動実行させる
定義したshared_contextを用いて、前述したモデルSpecを書き直してみます。
RSpec.describe Customer, type: :model, relation_specs: true do
end
# 下記のコードが実行されるのと同じ
# RSpec.describe Customer, type: :model do
# describe 'association' do
# describe 'has_many' do
# it { is_expected.to have_many(:orders).dependent(:destroy) }
# end
# end
# end
RSpec.describe Order, type: :model, relation_specs: true do
end
# 下記のコードが実行されるのと同じ
# RSpec.describe Order, type: :model do
# describe 'association' do
# describe 'belongs_to' do
# it { is_expected.to belong_to(:customer) }
# end
# end
# end
今回の例だと一つのリレーションだけなので恩恵が感じられませんが、実際のサービスで使うような複雑なモデルだと、10個以上のモデルとリレーションを持っているのも珍しくないと思います。そう言ったモデルのSpecに関しては、リレーションのSpecを書く手間・書き直す手間を省くこともできますし、Specファイル自体の行数を抑えることで、コードの見通しを良くすることにも繋がります。
最後に
RSpecのコードを読み込めば、もっとスマートに実装できそう?