読者です 読者をやめる 読者になる 読者になる

Going Rails-way

Ruby on Rails 道を突き詰めたい

共通の振る舞いのspec

継承やmixinで振る舞いを共有するクラスがあるとき、共有する振る舞いのテストを個別に書いてたり、特定のクラスでだけ書くようでは、不十分だと思いました。

振る舞いが他の実装との兼ね合いで変わってしまったり、オーバーライドによって挙動が変わるなどしては困ります。

また、抽象クラスやモジュールでインターフェースのみ定義しておき、具象クラスでそれを実装するという仕様のとき、それが守られているかどうか確認したいことがありますが、すべてのクラスのテストでいちいち書くのも面倒だったりします。

このような共通の振る舞いのspecについて、最近はこんな感じで書いています。

共通する振る舞いのテストを使う

こうすることで、ClassAincludeしているモジュールの想定している振る舞いが実装されていることをテストでき、また理解しやすくなります。

describe ClassA do
  # ~のように振る舞うことをテスト
  it_behaves_like 'HogeExtension', described_class.new
  it_behaves_like 'FugaExtension', described_class.new
  it_behaves_like 'HelloClass'

  # ClassA独自の振る舞いのテスト
  describe '#hello' do
    subject { described_class.new.hello }
    it { is_expected.to eq('ClassA world') }
  end
end

テスト対象

module HogeExtension
  def hoge
    'hoge'
  end
end

module FugaExtension
  def fuga
    'fuga'
  end
end

class HelloClass
  def hello
    raise NotImplementedError # このメソッドはオーバーライドする想定(インターフェースのみ定義するなど)
  end
end

class ClassA < HelloClass
  include HogeExtension
  include FugaExtension

  def hello
    'ClassA world'
  end
end

共通する振る舞いをまとめる

include HogeExtensionしたクラスのインスタンスの振る舞いのテストをこんな感じでshared_examples_forでまとめてみます。

ブロック引数objとしてインスタンスを受け取るようにしておきました。

# spec/shared/examples/hoge_extension.rb
shared_examples_for 'HogeExtension' do |obj|
  describe '#hoge' do
    subject { obj.hoge }
    it "returns 'hoge'" do
      is_expected.to eq('hoge')
    end
  end
end

同様にFugaExtension

# spec/shared/examples/fuga_extension.rb
shared_examples_for 'FugaExtension' do |obj|
  describe '#fuga' do
    subject { obj.fuga }
    it "returns 'fuga'" do
      is_expected.to eq('fuga')
    end
  end
end

HelloClassクラスについては、インスタンスの生成方法がわかっているということにして、described_classを使いつつ、#helloが実装されていること、Stringを返すことをテストしてみました。

# spec/shared/examples/hello_class.rb
shared_examples_for 'HelloClass' do
  it { expect(described_class.ancestors).to be_include(HelloClass) }
  describe '#hello' do
    subject { described_class.new.hello }
    it { expect { is_expected }.to_not raise_error(NotImplementedError) }
    it { is_expected.to be_an_instance_of(String) }
  end
end