rspecのrspecに学ぶ、ネストの深いrspecを書かない方法
ネストの深いrspec
BDDは、下記のように書くので、GivenとWhenを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の流儀
rpec-coreのrspecを読んだところ、指針らしきものが見えてきた。結論から言うと、rspecのrspecではとても愚直にrspecを記述している。
contextにbefore、letが無くてもいい
- contextのbeforeやletは無理して使わない。
- itの中で事前条件のためのセットアップをすることが多い
https://github.com/rspec/rspec-core/blob/main/spec/rspec/core_spec.rb#L122-L130
itの中身は一行でなくてもいい
- 事前条件のセットアップを含めて、そこそこ長い記述をすることもある
- ただし、これが許されるのはあくまで1つの振る舞いを確認している場合。複数の振る舞いを1つのitで確認することは無い
itの中身に事前条件のセットアップを含んでもいい
- というかほとんどそうしている
is_expectedは無理に使わない
- ほぼ見かけなかった
itに複数のexpectを書いていい
- 1つの振る舞いに関することであれば1つのitに複数のexpectを書く
- 1つの振る舞いを観測するために複数の検証が必要なことは普通にある
itは振る舞いで分割する
- expectを複数書くのを許容しているが、何でもかんでも書いているわけではない。
- 外から見た動きとして特徴づけられることごとにitを分ける
context内でメソッド定義してitの中身を短くする
- subjectやletは無理に使わず普通にメソッド定義する
- ちなみに、context内でのメソッド定義はcontextローカルなヘルパーとして登録される。
rspecの書き方のミソ
どうやら、rspecを書く上で重要なのは下記二点のよう。subjectとかはこれを満たした上で使えたら使おう。
- 1つの振る舞いにつき1つのit(振る舞いの単位は観測者によって変わる)
- contextとitに書いてあることが英文として読みやすいように構成する