LoginSignup
33
28

More than 5 years have passed since last update.

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

Posted at

概要

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

33
28
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
28