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

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

### 環境
Ruby: 2.3.1
Rails: 5.0

### 目次

- モデル間のリレーションSpecについて
- 自動生成させるモジュールを作る
- 実際に自動実行させる

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

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

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

```rb:customer.rb
class Customer < ApplicationRecord
  has_many :orders, dependent: :destroy
end
```

```rb:order.rb
class Order < ApplicationRecord
  belongs_to :customer
end
```

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

```rb: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
```

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

```rb: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
```

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

```rb: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を書き直してみます。

```rb: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
```

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