この記事のテーマ
Ruby のテスティングフレームワーク RSpec を使ったテストにおいて、 ひとつの shared_context や shared_examples を複数のファイルやテストで使い回す ことについての私なりの解法を書いています。
RSpec では、同じ内容のテストを違う対象を相手に実施することが多いです。
特にバリデーションを検証するのに「長さ〇の文字列をセットしてバリデーションを実行」というのを複数のモデル/フィールドで確認することになります。
同じようなテストケースは使い回したいのですが、shared_context や shared_examples を複数のファイルで使い回すための情報が意外に転がってないと感じました。
色々と試行錯誤した結果をこの記事にまとめてみました。同じことを考えている方の一助になれば嬉しいです。
最初に要点
このあとに自分なりの試行錯誤を書いていますが、読むのが面倒という方は以下だけ押さえておけばOKだと思います。
- 共用する shared_context や shared_examples をファイルに定義
- 使用する側で shared_context のあるファイルを
require
- include_context や it_behaves_like で利用
参考にしたもの
RSpec そのものの解説や、テストの書き方についての説明については、様々なエントリがあります。個人的に参考になったものを以下に紹介します。
- RSpec 公式サイト
-
Relish
RSpec に関するドキュメント群。特に「RSpec Expectations」にあるマッチャの種類やサンプルはとても参考になりました。 - スはスペックのス 【第 1 回】 RSpec の概要と、RSpec on Rails (モデル編)
- 使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
環境(主要なもののみ)
- macOS 10.14.3 Mojave
- ruby 2.6.1p33
- Rails 5.2.2
- rspec-rails 3.8.2
検証用アプリケーションについて
記事を書くために作成した rspec_samples というアプリを Github で公開しています。
書籍と会員情報を扱うという想像のもと、 Book と Model という2つのモデルを作成しました。
この記事では presence バリデーションを検証することについてのみ詳しく書きましたが、アプリには length や uniqueness を検証するための shared_context や shared_examples も作ってあります。
Rails.root/spec 以下の support および models ディレクトリにあるファイルをご確認ください。
あくまで動作確認が目的なので、全て scaffold で作成したなど作りが雑です。そのあたりは目をつぶって貰えるとありがたいです。
テスト内容
この記事では Book モデルの title プロパティに関する presence バリデーションが機能するかを検証する shared_context を作成します。
presence:true
が機能しているかどうかを確認しますので、検証内容は以下の2つとします。
- nil をセットして検証を実行すると結果が invalid になるか(invalid になるのが正しい)
- 空文字をセットして検証を実行すると結果が invalid になるか(同上)
テストの作成
Book モデルの title プロパティについて検証ロジックを作成してから、それを他のモデル/プロパティでも使えるように改造していきます。
- 基本となるテストの作成
- 複数のテストで使えるよう検証ロジックを独立
- 修正した検証ロジックをテストとは別のファイルに切り出す
- 切り出したファイルを別のファイルから読み込む
基本となるテストの作成
presence を検証するテストを以下のように定義しました。
describe:presence
の中で context を2つに分けず、1つの it 内で nil も空文字もテストするなど、方法は色々あると思います。
# タイトルのバリデーションを検証
context :title do
let(:book) { build(:book, title: title) }
describe :presence do
context :nil do
let(:title) { nil }
it { expect(book).to be_invalid }
end
context :blank do
let(:title) { '' }
it { expect(book).to be_invalid }
end
end
# 以下略
複数のテストで使えるよう検証ロジックを独立
上記では nil と空文字それぞれについて別々の context で検証していますが、異なるのは let(:title) の値だけで、それ以外は期待する入力も結果も全く同じです。
そこで、この context を共用できるようにまとめます。
まとめる方法はいくつかありますが shared_context は引数を取ることができますので、以下の2通りを考えます。
(ちなみに shared_context だけでなく shared_examples も引数を取ることができます)
- 引数から値を得る shared_context
- 引数がなく let で値を得る shared_examples
引数から値を得る shared_context
- context の文字列を type の値次第で変えて、失敗した時にその箇所がわかるようにしています。
- 第1引数 type が :nil なら title に nil を入れた場合、:empty なら空文字を入れた場合を検証します。
nil は NG で空文字は OK、という要件にも対応できるよう、引数 expected で nil と空文字のどちらをテストするか指定できるようにしています。 - 第2引数 expected には、期待する結果を true(=valid)/false(=invalid) で指定します。
shared_context :presence_validation do |type, expected|
let(:actual) { build(factory, attribute => value) }
context (type == :nil ? 'nil' : 'empty') do
let(:value) { type == :nil ? nil : '' }
it { expect(actual).to (expected ? be_valid : be_invalid) }
end
end
この shared_context は以下のように使います。
let(:factory) { :book }
let(:attribute) { :title }
include_context :presence_validation, :nil, false
include_context :presence_validation, :empty, false
引数がなく let で値を得る shared_examples
shared_context で引数を取る格好にすると RSpec の DSL とは異質なコードがテストに混在してしまいます。
可能な限り RSpec の DSL だけで完結させられるように shared_examples を以下のように定義しました。
- let(:type) には :nil か :empty かを指定します(意味合いは shared_context の第1引数と同じ)。
- let(:expect) には期待する結果を指定します(shared_context の第2引数と同じ)。
shared_examples :presence_example do
it :validate_presense do
actual = build(factory, attribute => (type == :nil ? nil : ''))
if expected
expect(actual).to be_valid
else
expect(actual).to be_invalid
end
end
end
この shared_examples は以下のように使います。
let(:factory) { :book }
let(:attribute) { :title }
describe :presence do
let(:expected) { false }
context :nil do
let(:type) { :nil }
it_behaves_like :presence_example
end
context :empty do
let(:type) { :empty }
it_behaves_like :presence_example
end
end
修正した検証ロジックをテストとは別のファイルに切り出す
ここまでで定義した shared_context や shared_examples はファイルをまたがって使い回すことはできません。そこでこれを別ファイルに切り出します。
rails_helper.rb にある下記の行を有効にしておいてください。
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
- 切り出す先のファイルを作成
ここでは Rails.root/spec/support/shared_contexts3.rb というファイルに切り出すことにします。 - shared_context をコピーして以下のように修正
shared_context を RSpec.shared_context とします。切り出した箇所は元のファイルから削除しておいてください。
RSpec.shared_context :presence_validation do |type, expected|
let(:actual) { build(factory, attribute => value) }
context (type == :nil ? 'nil' : 'empty') do
let(:value) { type == :nil ? nil : '' }
it { expect(actual).to (expected ? be_valid : be_invalid) }
end
end
切り出したファイルを元のファイルから読み込む
共用する shared_context があるファイルを require
して完了です。
切り出した元のファイルだけでなく、別のファイルからでも同じように require
すれば検証内容がそのまま使えます。
require 'support/shared_contexts3'
最後に
以上、 shared_context や shared_examples を使い回すための方法について、私なりの解法をまとめました。
describe や context の使い分け、shared_context や shared_examples の使い方には様々なバリエーションが考えられると思います。
例えば Relish の このページ にはメソッドを shared_context に定義して、そのメソッドをテストから呼び出すということもしています。
この記事に書いたのとは違うやり方もあるとか、そのやり方は好ましくないなどがあれば、コメント頂けるととてもありがたいです。