50
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

rspecのrspecに学ぶ、シンプルなrspecの書き方

Last updated at Posted at 2021-05-12

無駄に複雑な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の記述は許容されて良いものであるということがわかる。

関連記事

今回見出したテスト記述の指針は、下記の記事で紹介されている「雑な書き方」によく似ています。

50
30
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?