継承やmixinで振る舞いを共有するクラスがあるとき、共有する振る舞いのテストを個別に書いてたり、特定のクラスでだけ書くようでは、不十分だと思いました。
振る舞いが他の実装との兼ね合いで変わってしまったり、オーバーライドによって挙動が変わるなどしては困ります。
また、抽象クラスやモジュールでインターフェースのみ定義しておき、具象クラスでそれを実装するという仕様のとき、それが守られているかどうか確認したいことがありますが、すべてのクラスのテストでいちいち書くのも面倒だったりします。
このような共通の振る舞いのspecについて、最近はこんな感じで書いています。
共通する振る舞いを使う
こうすることで、ClassA
がinclude
しているモジュールの想定している振る舞いが実装されていることをテストでき、また理解しやすくなります。
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