【RSpec】shared_examples の使い方
概要
Rails の現場では、 shared_examples
や shared_context
を使ってテストを共通化するのを良く見かけます。
改めてどんな使い方をするのか調べたので、ここに残しておきます。
環境
ruby 3.1.2
rails 7.0.8
どんなときに使う?
以下の2つの class が存在するとします。
SomeDummyKlass1
SomeDummyKlass2
この2つのクラスで、似たような振る舞いをするときに、それぞれに単体テストを書くのは面倒です。
そんなときに使うのが shared_examples
や shared_context
です。
shared_context との違い
ここでは shared_examples に焦点を当てているので割愛しますが、
以下のようなイメージを持っているとスッキリすると思います。
-
shared_examples
は期待値の検証を共通化 -
shared_context
は条件を共通化
個人的には shared_examples
を使っている現場が多いですし、好きです。
shared_examples
は spec ファイルを見ただけでテスト内容をが分かりやすいです。
一方、shared_context
は spec ファイルだけではテストを読み取れず、どんな条件なのかを見に行く必要があってめんどくさい印象です。
shared_examples の基本的な使い方
先程の SomeDummyKlass1
と SomeDummyKlass2
を共通化することを想定します。
以下のような 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箇所にまとめてくれるのは便利です。
次に、それぞれのクラスの単体テストで、以下のように記述します。
require 'rails_helper'
RSpec.describe SomeDummyKlass1 do
describe "include_examples に引数を渡すテスト" do
include_examples "共通化したいテスト", "あああ", "いいい", "ううう"
end
end
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 のテストは便利
例えば、SomeDummyKlass1
と SomeDummyKlass2
で以下のようなモジュールをインクルードしていたとします。
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