Posted at

モデル間のリレーションSpecを書きたくないので自動生成してみる

More than 1 year has passed since last update.


概要

モデル間のhas_many、has_one、belongs_toをテストするためのSpecを自動生成して実行させます。ERBでコード生成してあれこれする感じです。


環境

Ruby: 2.3.1

Rails: 5.0


目次


  • モデル間のリレーションSpecについて

  • 自動生成させるモジュールを作る

  • 実際に自動実行させる


モデル間のリレーションSpecについて

RSpecにはモデル間のリレーションをテストするためのマッチャーが用意されています。

例えば、CustomerモデルとOrderモデルがあるとします。Ordercustomer_idを保持しており、一対多の関係です。


customer.rb

class Customer < ApplicationRecord

has_many :orders, dependent: :destroy
end


order.rb

class Order < ApplicationRecord

belongs_to :customer
end

これらモデル間のリレーションSpecは、例えば以下のように書くことができます。


customer_spec.rb

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


order_spec.rb

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配下に作りました。


lib/auto_spec/model_spec.rb

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



lib/auto_spec/templates/relations.erb

<% 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を定義して、その中で実行させるようにします。


spec/support/shared_contexts/relation_contexts.rb

shared_context :relation_specs, relation_specs: true do

extend AutoSpec::ModelSpec
exe_relation_specs
end


実際に自動実行させる

定義したshared_contextを用いて、前述したモデルSpecを書き直してみます。


customer_spec.rb

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


order_spec.rb

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のコードを読み込めば、もっとスマートに実装できそう?