マイペースなRailsおじさん

Ruby、Ruby on Rails、オブジェクト指向設計を主なテーマとして扱います。だんだん大きくなっていくRuby on Rails製プロダクトのメンテナンス性を損なわない方法を考えたり考えなかったりしている人のブログです。

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が入っていてなんかしている事が多い。[要出典]

私は異端者なのだろうか。 なんか腑に落ちないので、rspecrspecを読んでみた。

rspecrspecの流儀

rpec-coreのrspecを読んだところ、指針らしきものが見えてきた。結論から言うと、rspecrspecではとても愚直にrspecを記述している。

github.com

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で確認することは無い

https://github.com/rspec/rspec-core/blob/04d5e5264daf51bac0e40573381eaea356737883/spec/rspec/core_spec.rb#L180-L208

itの中身に事前条件のセットアップを含んでもいい

  • というかほとんどそうしている

is_expectedは無理に使わない

  • ほぼ見かけなかった

itに複数のexpectを書いていい

  • 1つの振る舞いに関することであれば1つのitに複数のexpectを書く
  • 1つの振る舞いを観測するために複数の検証が必要なことは普通にある

https://github.com/rspec/rspec-core/blob/04d5e5264daf51bac0e40573381eaea356737883/spec/rspec/core_spec.rb#L291-L295

itは振る舞いで分割する

  • expectを複数書くのを許容しているが、何でもかんでも書いているわけではない。
  • 外から見た動きとして特徴づけられることごとにitを分ける

https://github.com/rspec/rspec-core/blob/04d5e5264daf51bac0e40573381eaea356737883/spec/rspec/core_spec.rb#L164-L178

context内でメソッド定義してitの中身を短くする

  • subjectやletは無理に使わず普通にメソッド定義する
  • ちなみに、context内でのメソッド定義はcontextローカルなヘルパーとして登録される。

https://github.com/rspec/rspec-core/blob/04d5e5264daf51bac0e40573381eaea356737883/spec/integration/order_spec.rb#L195-L197

rspecの書き方のミソ

どうやら、rspecを書く上で重要なのは下記二点のよう。subjectとかはこれを満たした上で使えたら使おう。

  • 1つの振る舞いにつき1つのit(振る舞いの単位は観測者によって変わる)
  • contextとitに書いてあることが英文として読みやすいように構成する