ls /asapon/blog

基本tech、時々多趣味

RSpecで書く基底クラスのテスト実践例

これはなにか

RSpecを書くときに役立つ、実践例を書いた記事です。今回は

  • 基底クラス

のテストを、改善前と改善後合わせて紹介します。他テスト(Minitestや他プログラミング言語)でも応用できる考え方なので、参考にしてみてください。

基底クラスをテストする

クラス例は以下の base.rb になります。
それぞれのメソッドは

  • #greet
    • 継承先で実装される想定
    • ここでは NotImplementedError をraiseするだけ
  • #default_language
    • 初期値は :ja
    • 継承先の必要に応じてオーバーライドされる

といった単純なものになっています。
このクラスをどのようにテストするか考えていきます。

# base.rb

class Base
  def initialize; end

  def greet
    raise NotImplementedError
  end

  def default_language
    :ja
  end
end

改善前

普通に実装すると以下のようになると思います。
やっていることは Base クラスをインスタンス化し、それぞれのメソッドをテストしているだけです。

# base_spec.rb

require_relative '../base'
require 'spec_helper'

RSpec.describe Base do
  let(:baes) { described_class.new }

  describe '#greet' do
    it 'raises NotImplementedError' do
      expect { baes.greet }.to raise_error(NotImplementedError)
    end
  end

  describe '#default_language' do
    it 'returns :ja' do
      expect(baes.default_language).to eq :ja
    end
  end
end

このテストの改善点

このテストには気になる点があります。それは

Baseクラスそのものをインスタンス化している

いう点です。
本来Baseクラスは、継承先で使われることを想定して作られているはずです。そのため、Baseクラスそのものをインスタンス化するといった使われ方は想定しておらず、実態とは異なる使い方でテストしていることになります。
基底クラスをテストするときは、安直にインスタンス化せず、継承させたあとにテストするべきです。

改善後

下が改善した基底クラスのテストになります。ポイントは

  • Class.#new で派生クラスを作っている
  • 派生クラスをインスタンス化し、メソッドを呼んでいる

という点です。
派生クラスはClassを利用しています。これで簡単に、ガワになる継承先を作ることができました。
また、オーバーライド後の動作も確認できるように

  • when #default_language is overrided to :en

といったテストも作ることができました。より実践に沿った、テストケースになったと思います。

# base_spec.rb

require_relative '../base'
require 'spec_helper'

RSpec.describe Base do
  let(:child_class) do
    Class.new(described_class)
  end
  let(:child_class_instance) { child_class.new }

  describe '#greet' do
    it 'raises NotImplementedError' do
      expect { child_class_instance.greet }.to raise_error(NotImplementedError)
    end
  end

  describe '#default_language' do
    it 'returns :ja' do
      expect(child_class_instance.default_language).to eq :ja
    end

    context 'when #default_language is overrided to :en' do
      let(:child_class) do
        Class.new(described_class) do
          def default_language
            :en
          end
        end
      end

      it 'returns :en' do
        expect(child_class_instance.default_language).to eq :en
      end
    end
  end
end