無駄に複雑なrspec
rspecで書いたテストケースは、contextのネストが深くなることがある。
BDDのテストケースは下記の形式をとるので、これをcontextで表現しようとすると難しいことがある。
Given:最初の文脈(前提)があって、
When:イベントが発生した場合、
Then:なんらかのアウトプットを保証する。
例えば、fizz_buzz問題でfizzを出力する振る舞いを確認する場合はこうなる。事前条件は必要ないので書いていない。シンプルで、contextとitをつなげてみたときにわかりやすい英文として記述できる。
describe "#fizz_buzz" do
subject { fizz_buzz(input) }
context "when input is multiple of 3" do
let!(:input) { 3 }
it { is_expected to eq "fizz" }
end
end
ここで、3の倍数に対する入力がfizzであることを確認するのに、3だけを確認するのは心細いことに気づく。
上述のフォーマットを崩さないように、itの中身を変えないことを意識するとこうなる。
describe "#fizz_buzz" do
subject { fizz_buzz(input) }
context "when input is multiple of 3" do
let!(:input) { 3 }
it { is_expected to eq "fizz" }
end
context "when input is multiple of 3" do
let!(:input) { 12 }
it { is_expected to eq "fizz" }
end
context "when input is multiple of 3" do
let!(:input) { 303 }
it { is_expected to eq "fizz" }
end
end
同じcontextが横並びしてしまった。5の倍数や3と5の倍数のパターンも記述することを考えると、3の倍数のパターンはひとくくりにしたほうがわかりやすそうだ。
describe "#fizz_buzz" do
subject { fizz_buzz(input) }
context "when input is multiple of 3" do
context "inputting 3" do
let!(:input) { 3 }
it { is_expected to eq "fizz" }
end
context "inputting 12" do
let!(:input) { 12 }
it { is_expected to eq "fizz" }
end
context "inputting 303" do
let!(:input) { 303 }
it { is_expected to eq "fizz" }
end
end
end
ネストが深くなってしまった。context "inputting 3" do
というのは、letで与えている数字と重複しているし、そもそも3を選んだことにはあまり意味はない。3の倍数であれば何でも良かったのだが、contextにまで顔を出してしまっている。
愚直にこうしてはいけないのだろうか?is_expected
のようなおしゃれな構文を手放した代わりに、ネストが浅く、単純で理解しやすいexpectが並んでいる。
describe "#fizz_buzz" do
context "when input is multiple of 3" do
it "returns fizz" do
expected = "fizz"
expect(fizz_buzz(3)).to eq expected
expect(fizz_buzz(12)).to eq expected
expect(fizz_buzz(303)).to eq expected
end
end
end
rspecの書き方に関する議論
どうも、Qiitaなどで見かける記事では、subjectを使おうとか、contextを細かく分けていたり、またそのcontextの中にちょっとずつbeforeが入っていてなんかしている事が多い。[要出典]
私は異端者なのだろうか。
なんか腑に落ちないので、rspecのrspecを読んでみた。
rspecのrspecの流儀
rspec-coreのrspecを読んだところ、指針らしきものが見えてきた。結論から言うと、rspecのrspecではとても愚直にrspecを記述している。
contextにbefore、letが無くてもいい
- contextのbeforeやletは無理して使わない。
- itの中で事前条件のためのセットアップをすることが多い
context "when RSpec.configuration.format_docstrings is set to a block" do
it "formats the description using the block" do
RSpec.configuration.format_docstrings { |s| s.upcase }
example_group.example { }
example_group.run
pattern = /EXAMPLE AT #{relative_path(__FILE__).upcase}:#{__LINE__ - 2}/
expect(example_group.examples.first.description).to match(pattern)
end
end
itの中身は一行でなくてもいい
- 事前条件のセットアップを含めて、そこそこ長い記述をすることもある
- ただし、これが許されるのはあくまで1つの振る舞いを確認している場合。複数の振る舞いを1つのitで確認することは無い
it "clears examples, failed_examples and pending_examples" do
reporter.start(3)
pending_ex = failing_ex = nil
# (中略)事前条件用のセットアップ処理が続く
RSpec.clear_examples
reporter.register_listener(listener, :dump_summary)
expect(listener).to receive(:dump_summary) do |notification|
expect(notification.examples).to be_empty
expect(notification.failed_examples).to be_empty
expect(notification.pending_examples).to be_empty
end
reporter.start(0)
reporter.finish
end
itの中身に事前条件のセットアップを含んでもいい
- というかほとんどそうしている
is_expectedは無理に使わない
- ほぼ見かけなかった
itに複数のexpectを書いていい
- 1つの振る舞いに関することであれば1つのitに複数のexpectを書く
- 1つの振る舞いを観測するために複数の検証が必要なことは普通にある
it 'returns the absolute location of the exe/rspec file' do
expect(File.exist? RSpec::Core.path_to_executable).to be(true)
expect(File.read(RSpec::Core.path_to_executable)).to include("RSpec::Core::Runner.invoke")
expect(File.executable? RSpec::Core.path_to_executable).to be(true) unless RSpec::Support::OS.windows?
end
itは振る舞いで分割する
- expectを複数書くのを許容しているが、何でもかんでも書いているわけではない。
- 外から見た動きとして特徴づけられることごとにitを分ける
it "clears example groups" do
RSpec.world.example_groups << :example_group
RSpec.clear_examples
expect(RSpec.world.example_groups).to be_empty
end
it "resets start_time" do
start_time_before_clear = RSpec.configuration.start_time
RSpec.clear_examples
expect(RSpec.configuration.start_time).not_to eq(start_time_before_clear)
end
describeやcontext内でメソッド定義してitの中身を短くする
- subjectやletは無理に使わず普通にメソッド定義する
- ちなみに、describe, context内でのメソッド定義はcontextローカルなヘルパーとして登録される。
RSpec.describe 'command line', :ui do
#(中略)
def examples(group)
yield split_in_half(stdout.string.scan(/^\s+#{group} example.*$/))
end
rspecに学ぶrspecの書き方のミソ
どうやら、rspecを書く上で重要なのは下記二点のよう。subjectとかis_expectedとかはこれらを満たした上で使えたら使おう。
- 1つの振る舞いにつき1つのit(振る舞いの単位は観測者によって変わる)
- contextとitに書いてあることが英文として読みやすいように構成する
これらの考え方に沿うと、冒頭で紹介したis_expected
を使わないrspecの記述は許容されて良いものであるということがわかる。
関連記事
今回見出したテスト記述の指針は、下記の記事で紹介されている「雑な書き方」によく似ています。