Help us understand the problem. What is going on with this article?

俺のRSpecがこんなに雑なわけがない

More than 3 years have passed since last update.

ここ最近、 RSpec の書き方が自分の中で変化したので、そのことを書きます。(こういう話ちょっと今更なきもしますが...)
私の書き方は少数派だろうと思います。他の人に強く勧めるつもりもありません。
ただ、どうにも Better Spec のような書き方には疲れました。
テストを書くということはもっと気楽なものでいいように思います。
こういう書き方もあるんだよ、という程度で読んでもらえれば幸いです。

概要

ほんの一年くらい前まで Better Spec みたいな RSpec の書き方を私も心がけていました。
describecontext を使い分け、subjectit をうまいこと組み合わせてテストを作っていました。

簡単な例ですが、次みたいな感じです。

# 以下、このテストを「ケースA」と呼びます。
describe 'Book' do
  describe '#valid?' do
    subject { book.valid? }

    context '必須項目を入力するとき' do
      let(:book) { Book.new(title: '孤独のグルメ 【新装版】', author: '久住 昌之・谷口 ジロー') }

      it { is_expected.to be_truthy }
    end

    context '必須項目を入力しないとき' do
      let(:book) { Book.new }

      it { is_expected.to be_falsey }

      # subjectと異なるメソッドをテストするときはitではなくspecifyを使う
      specify 'エラーメッセージがあること' do
        expect(book.errors.full_messages).to contain_exactly(
          'タイトルを入力して下さい',
          '筆者を入力して下さい'
        )
      end
    end
  end
end

3年くらいこの書き方が読みやすいと思っていたんですが、最近は次のような感じです。

# 以下、このテストを「ケースB」と呼びます。
describe 'Book' do
  describe '#valid?' do
    example '必須項目を入力するケース' do
      book = Book.new(title: '孤独のグルメ 【新装版】', author: '久住 昌之・谷口 ジロー')

      # is_asserted_by は https://github.com/joker1007/rspec-power_assert のメソッド
      is_asserted_by { book.valid? }
    end

    example '必須項目を入力しないケース' do
      book = Book.new

      is_asserted_by { !book.valid? }
      is_asserted_by { book.errors.full_messages.include?('タイトルを入力して下さい') }
      is_asserted_by { book.errors.full_messages.include?('筆者を入力して下さい') }
    end
  end
end

雑!!

subjectcontext でテストを説明していた前者に比べて、後者は example だけです。
(念の為、 exampleitspecify は同じメソッドです)
なんとなく、後者は雑に見えませんか?
前者の書き方に慣れていた私には雑に見えます。しかし、私はこの書き方をしています。

雑なRSpecの書き方

次のようなことを気をつけて書いています。

  • it メソッドは example を使う
  • describecontext によるネストは最小限にする
  • コードを DRY にするために letbefore を使う

やりたいことは何か?

やりたいことは example 句を読めば理解できるテストを作ることです。

ケースAの書き方で私が不満なことは、目線が上下に動くことです。
私の目線は contextlet を読んだ後 it を読む時に一度 subject に戻ります。
短いテストならいいですが、長くなってくると辛いです。

もうひとつケースAで不満なことは、テストケースを追加する時に、ネスト構造を意識する必要があることです。
新しいテストケースを追加するときは、describecontextsubject で作られたネスト構造を壊さないように追加するか、全く別のネスト構造を作る必要があります。
(汚い手ですが、既存のネスト構造を無視して書く方法もあります...)

私は上記のような事態に疲れました。
it 句が subjectcontext に依存しているために気軽にテストを追加できないのです。
そのため、なるべく it 句を読めばわかるようなテストを書くことにしました。
(subject を使わない場合 it は不自然なので example を使うことにしました)

describecontext によるネスト構造もなるべく使わないように心がけています。
if 文やループ文でネストが深くなるとコードが読みづらくなることはよく知られていることです。
私は describecontext のネストも複雑になりがちであると思います。

letbeforeit 句から離れると、 it 句が実行される時の状態が読みづらくなります。
describecontext のネストの中に letbefore が織り交ぜられると余計にわかりづらくなります。
極力 it 句だけで理解できるように心がけますが、 it 句だけでは DRY にかけないことがあるので、そのような場合だけ letbefore を使うことにしています。

it 句に集中することでテストをシンプルにします。

ただ、この方法にはデメリットもあります。

シンプルさのために諦めたこと

ケースBの方法は it 句に多くのものを詰め込むことで、 it 句だけ読めば理解できるテストを目指します。
これはシンプルなテストを作ることができますが、一方次のようなことは諦めました。

  • format doc の出力結果を綺麗にすること
  • 単一条件テスト (1つの it の中で 1つの条件だけテストすること)

Power Assert について

ケースBでは is_asserted_by という matcher を使っています。
見慣れない方が多いと思いますが、これは joker1007/rspec-power_assert gem をいれることで使えるようになる PowerAssert の matcher です。

僕は Power Assert が好きで、例外と配列の比較以外は Power Assert でいいかなと思っています。
須藤さんに怒られそうですが... (参考: https://www.youtube.com/watch?v=g33d9SSbvd8)

僕としては Power Assert を使うことで matcher について調べる時間を短縮できる事がとても助かっています。
毎回ほぼ is_asserted_by だけでいいし、 is_asserted_by のブロック内の書き方は Ruby の文法で済むので調べる必要はありません。
Power Assert が失敗した時のメッセージが分かりづらいケースがあるというのは、そのとおりですが、エラーメッセージを読むときはテストコードも一緒に読むし、だいたいの場合理解できるのではないかと楽観的に考えています。

基本的にテストが落ちるのはTDDを行っている時だと思うので、テストがどのように落ちるかもある程度予想しながら書いていると思います。
その場合、多少エラーメッセージが読みづらくてもいいかなと思います。わりと十分わかりますし。。。

後からテストコードを読む時も、is_asserted_by のブロック内は簡単な Ruby のコードなので読めると思います。

参考

影響を受けたもの

念の為タイトルの元ネタ: 俺の妹がこんなに可愛いわけがない

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away