ここ最近、 RSpec の書き方が自分の中で変化したので、そのことを書きます。(こういう話ちょっと今更なきもしますが...)
私の書き方は少数派だろうと思います。他の人に強く勧めるつもりもありません。
ただ、どうにも Better Spec のような書き方には疲れました。
テストを書くということはもっと気楽なものでいいように思います。
こういう書き方もあるんだよ、という程度で読んでもらえれば幸いです。
概要
ほんの一年くらい前まで Better Spec みたいな RSpec の書き方を私も心がけていました。
describe
と context
を使い分け、subject
と it
をうまいこと組み合わせてテストを作っていました。
簡単な例ですが、次みたいな感じです。
# 以下、このテストを「ケース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
雑!!
subject
や context
でテストを説明していた前者に比べて、後者は example
だけです。
(念の為、 example
と it
と specify
は同じメソッドです)
なんとなく、後者は雑に見えませんか?
前者の書き方に慣れていた私には雑に見えます。しかし、私はこの書き方をしています。
雑なRSpecの書き方
次のようなことを気をつけて書いています。
-
it
メソッドはexample
を使う -
describe
とcontext
によるネストは最小限にする - コードを DRY にするために
let
やbefore
を使う
やりたいことは何か?
やりたいことは example
句を読めば理解できるテストを作ることです。
ケースAの書き方で私が不満なことは、目線が上下に動くことです。
私の目線は context
と let
を読んだ後 it
を読む時に一度 subject
に戻ります。
短いテストならいいですが、長くなってくると辛いです。
もうひとつケースAで不満なことは、テストケースを追加する時に、ネスト構造を意識する必要があることです。
新しいテストケースを追加するときは、describe
、 context
、 subject
で作られたネスト構造を壊さないように追加するか、全く別のネスト構造を作る必要があります。
(汚い手ですが、既存のネスト構造を無視して書く方法もあります...)
私は上記のような事態に疲れました。
it
句が subject
や context
に依存しているために気軽にテストを追加できないのです。
そのため、なるべく it
句を読めばわかるようなテストを書くことにしました。
(subject
を使わない場合 it
は不自然なので example
を使うことにしました)
describe
や context
によるネスト構造もなるべく使わないように心がけています。
if
文やループ文でネストが深くなるとコードが読みづらくなることはよく知られていることです。
私は describe
や context
のネストも複雑になりがちであると思います。
let
や before
も it
句から離れると、 it
句が実行される時の状態が読みづらくなります。
describe
や context
のネストの中に let
や before
が織り交ぜられると余計にわかりづらくなります。
極力 it
句だけで理解できるように心がけますが、 it
句だけでは DRY にかけないことがあるので、そのような場合だけ let
や before
を使うことにしています。
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 のコードなので読めると思います。
参考
影響を受けたもの
- Go 言語のテストの書き方
- trailblazer
念の為タイトルの元ネタ: 俺の妹がこんなに可愛いわけがない