概要
複数のcontextを組み合わせる方法を思いついた
追記:
gemにした
- rspec_compose_context | RubyGems.org | your community gem host
- nanki/rspec_compose_context: Composable context for RSpec.
課題
Webアプリでちょっと複雑なフォームのspecを書くとする。
かなり単純化した例を考える。
- 項目
a
は1, 2の値を取りうる - 項目
b
は1, 2の値を取りうる - ただし、
a
が1の時のみ、b
の値を入力することができる - それとは関係なく、
c
は1, 2の値を取りうる
よくある風
全6通りのパターンをよくある風に書き下すと下のような感じになる。
- 本質的ではないネスト構造が邪魔でメンテナンス性が低い
- cに関しての記述が分散していて、c = 3とか増えた時に泣ける
- 可読性が低い
RSpec.describe "fukuzatsu na form" do
context "a = 1" do
subject do
a + b + c
end
let :a do
1
end
context "b = 1" do
let :b do
1
end
context "c = 1" do
let :c do
1
end
it { is_expected.to eq 3 }
end
context "c = 2" do
let :c do
2
end
it { is_expected.to eq 4 }
end
end
context "b = 2" do
let :b do
2
end
context "c = 1" do
let :c do
1
end
it { is_expected.to eq 4 }
end
context "c = 2" do
let :c do
2
end
it { is_expected.to eq 5 }
end
end
end
context "a = 2" do
subject do
a + c
end
let :a do
2
end
context "c = 1" do
let :c do
1
end
it { is_expected.to eq 3 }
end
context "c = 2" do
let :c do
2
end
it { is_expected.to eq 4 }
end
end
end
shared contextを使う
- 悪くはない
- specがfailした時に、
include_context
したcontextの名前は表示されないので、自分でcontext
の名前を書く必要がある。
RSpec.describe "fukuzatsu na form" do
shared_context 'a' do |value|
let :a do
value
end
end
shared_context 'b' do |value|
let :b do
value
end
end
shared_context 'c' do |value|
let :c do
value
end
end
context do
subject do
a + b + c
end
context "a = 1, b = 1, c = 1" do
include_context 'a', 1
include_context 'b', 1
include_context 'c', 1
it { is_expected.to eq 3 }
end
context "a = 1, b = 1, c = 2" do
include_context 'a', 1
include_context 'b', 1
include_context 'c', 2
it { is_expected.to eq 4 }
end
end
# 略
end
shared context + tag
shared_context
のタグを使った書き方でいいのではと思ったが、将来的にはこの書式には別の意味が与えられるので今から使うのは控えたい。(rspec-core3.5.0以降では新しい方がデフォルトになっている)
- shared contextにタグをつけて自動的にincludeする - Qiita
- Consider changing the semantics of shared example group metadata · Issue #1790 · rspec/rspec-core
RSpec::Parameterizedを使う
contextではないが、パラメータの組み合わせで済む場合はrspec-parameterized
を使うと簡潔な表現で書けるらしい。
使われたり使われなかったりするパラメータがある場合はどうするのがいいんだろう。
module_evalを使う
-
context
はRubyのモジュールなので、module_eval
で繋ぐことで動的にネストしている - 素直にrspecの構成要素を使っているので抵抗感が少ない
-
def ...
はなんとかしたい
PICTとか使う機会がたまにあるが、相性は良さそう。
よくある風に書くと、結果に合わせてspecをいじるのはかなり辛いはず。
def a(value)
proc do
context "a = #{value}" do
let :a do
value
end
end
end
end
def b(value)
proc do
context "b = #{value}" do
let :b do
value
end
end
end
end
def c(value)
proc do
context "c = #{value}" do
let :c do
value
end
end
end
end
def e(value)
proc do
it { is_expected.to eq value }
end
end
def abc
proc do
context do
subject do
a + b + c
end
end
end
end
def ac
proc do
context do
subject do
a + c
end
end
end
end
RSpec.describe "fukuzatsu na form" do
[
[abc, a(1), b(1), c(1), e(3)],
[abc, a(1), b(1), c(2), e(4)],
[abc, a(1), b(2), c(1), e(4)],
[abc, a(1), b(2), c(2), e(5)],
[ac, a(2), c(1), e(3)],
[ac, a(2), c(2), e(4)],
].map{|cs| cs.inject(self){|m, c| m.module_eval &c}}
end
まとめ
モジュールを動的にネストするのはRubyらしくて気に入っているが、そういったコードが好まれない環境もあると思われるので、shared_context
で頑張るのは割と良い気がする。
include_context
に渡すパラメータからcontext
の名前も生成するようなメソッドを書けば、ほぼ同じことができるし、そちらの方がいいのかもしれない。
context
の名前は人間が読みやすいものをつける慣習があり、shared_context
の名前は参照しやすい名前(例えばエディタの補完機能と相性のよいsnake_caseとか)を使いたいとの想いがあるところが悲劇の始まりだと思った。
it_behaves_like
の使い方はおかしいが下記のコードはこの点を解決する。
RSpec.describe "fukuzatsu na form" do
shared_context 'a' do |value|
metadata[:description] = "a = #{value}"
let :a do
value
end
end
shared_context 'c' do |value|
metadata[:description] = "c = #{value}"
let :c do
value
end
end
context do
subject do
a + c
end
it_behaves_like 'a', 1 do
it_behaves_like 'c', 1 do
it { is_expected.to eq 2 }
end
end
# 略
end
end