🐘

【RSpec】shared_examples の使い方

2024/01/21に公開

概要

Rails の現場では、 shared_examplesshared_context を使ってテストを共通化するのを良く見かけます。

改めてどんな使い方をするのか調べたので、ここに残しておきます。

環境

ruby 3.1.2
rails 7.0.8

どんなときに使う?

以下の2つの class が存在するとします。

  • SomeDummyKlass1
  • SomeDummyKlass2

この2つのクラスで、似たような振る舞いをするときに、それぞれに単体テストを書くのは面倒です。
そんなときに使うのが shared_examplesshared_context です。

shared_context との違い

ここでは shared_examples に焦点を当てているので割愛しますが、
以下のようなイメージを持っているとスッキリすると思います。

  • shared_examples は期待値の検証を共通化
  • shared_context は条件を共通化

個人的には shared_examples を使っている現場が多いですし、好きです。
shared_examples は spec ファイルを見ただけでテスト内容をが分かりやすいです。

一方、shared_context は spec ファイルだけではテストを読み取れず、どんな条件なのかを見に行く必要があってめんどくさい印象です。

shared_examples の基本的な使い方

先程の SomeDummyKlass1SomeDummyKlass2 を共通化することを想定します。
以下のような shared_examples を用意します。

RSpec.shared_examples "共通化したいテスト" do |arg1, arg2, arg3|
  let(:expect_value1) { "あああ" }
  let(:expect_value2) { "いいい" }
  let(:expect_value3) { "ううう" }

  it "uses the given parameter" do
    expect(arg1).to eq(expect_value1)
    expect(arg2).to eq(expect_value2)
    expect(arg3).to eq(expect_value3)
  end
end

「共通化したいテスト」の部分を後述する spec ファイルで呼び出すだけで、
このテストをどちらのクラスでも実行できるようになります。

まだこれくらいの記述なら2つのファイルにあってもいいかなと思いますが、
これが数百行とかになってきたら、テストのために同じこと書くのもしんどいですし、
何よりテストケースに漏れが出そうですよね。
それを上記のように1箇所にまとめてくれるのは便利です。

次に、それぞれのクラスの単体テストで、以下のように記述します。

spec/models/some_dummy_klass1_spec.rb
require 'rails_helper'

RSpec.describe SomeDummyKlass1 do
  describe "include_examples に引数を渡すテスト" do
    include_examples "共通化したいテスト", "あああ", "いいい", "ううう"
  end
end
spec/models/some_dummy_klass1_spec.rb
require 'rails_helper'

RSpec.describe SomeDummyKlass1 do
  describe "include_examples に引数を渡すテスト" do
    include_examples "共通化したいテスト", "あああ", "いいい", "ううう"
  end
end

include_examples の第1引数で、さきほどの「共通化したいテスト」という文字列を取ります。
第2引数以降で shared_examples で使用する引数を渡し、クラスごとに条件を変えることができます。

それぞれ実行してみると、どちらも普通にパスします。

> bundle exec rspec spec/models/some_dummy_klass1_spec.rb
.

Finished in 0.03359 seconds (files took 3.29 seconds to load)
1 example, 0 failures

> bundle exec rspec spec/models/some_dummy_klass2_spec.rb
.

Finished in 0.0373 seconds (files took 3.5 seconds to load)
1 example, 0 failures

ちょっと応用的にしてみる

module のテストは便利

例えば、SomeDummyKlass1SomeDummyKlass2 で以下のようなモジュールをインクルードしていたとします。

app/models/concerns/some_module.rb
module SomeModule
  extend ActiveSupport::Concern

  class_methods do
    def upcase(arg)
      arg.upcase
    end

    def downcase(arg)
      arg.downcase
    end
  end
end
class SomeDummyKlass1
  include SomeModule
end

class SomeDummyKlass2
  include SomeModule
end

このようなとき、upcase メソッドと downcase メソッドのテストを、それぞれのクラスに書くのは面倒です。shared_examplesを使うことで、 spec ファイル側では渡す第2引数以降を変えるだけで、期待値の検証を共通化することができます。

RSpec.shared_examples "SomeModule をインクルードしたときの upcase メソッドのテスト" do |arg|
  let(:expect_value) { arg.upcase }

  it "渡した引数が大文字になること" do
    expect(described_class.upcase(arg)).to eq(expect_value)
  end
end

RSpec.shared_examples "SomeModule をインクルードしたときの downcase メソッドのテスト" do |arg|
  let(:expect_value) { arg.downcase }

  it "渡した引数が小文字になること" do
    expect(described_class.downcase(arg)).to eq(expect_value)
  end
end
require 'rails_helper'

RSpec.describe SomeDummyKlass1 do
  describe "include_examples に引数を渡すテスト" do
    include_examples "SomeModule をインクルードしたときの upcase メソッドのテスト", "aaa"
    include_examples "SomeModule をインクルードしたときの downcase メソッドのテスト", "AAA"
  end
end
require 'rails_helper'

RSpec.describe SomeDummyKlass2 do
  describe "include_examples に引数を渡すテスト" do
    include_examples "SomeModule をインクルードしたときの upcase メソッドのテスト", "bbb"
    include_examples "SomeModule をインクルードしたときの downcase メソッドのテスト", "BBB"
  end
end

shared_examples に渡す引数によって検証するメソッドを切り替えることもできます。

RSpec.shared_examples "SomeModule をインクルードしたときのテスト" do |upcase_arg, downcase_arg|
  context ".upcase" do
    let(:expect_value) { upcase_arg.upcase }

    it "渡した引数が大文字になること" do
      expect(described_class.upcase(upcase_arg)).to eq(expect_value)
    end
  end

  context ".downcase" do
    let(:expect_value) { downcase_arg.downcase }

    it "渡した引数が小文字になること" do
      expect(described_class.downcase(downcase_arg)).to eq(expect_value)
    end
  end
end

呼び出し側

RSpec.describe SomeDummyKlass1 do
  describe "include_examples に引数を渡すテスト" do
    include_examples "SomeModule をインクルードしたときのテスト", "aaa", "AAA"
  end
end

ただ、個人的にはテストの見通しが悪くなるので、
shared_examples ごとに分けるほうがいいと思っています。

まとめ

shared_examples を使って、同じ期待値の検証をはまとめるようにしましょう!
ただ、テストは共通化しすぎると何のテストをしているか分かりにくくなるので、ほどほどに。

Discussion