はじめに:これは何?
僕がコードレビューしていて、「ん?いやいや、こんな書き方しちゃダメだよ!」と思ったコード例をまとめた記事です。
この記事でフォーカスするのはRSpecのDSLを適切に構築してないケースです。
ネタが増えたらまた追記するので、気になる方はこの記事のストックをお願いします!(記事の更新時に通知欄でお知らせします)
それでは以下が本編です👇
トップレベルのdescribeを2つ以上作らない
# NG
describe 'Foo spec' do
# ...
end
describe 'Bar spec' do
# ...
end
テストコードを読む際に、読み手は「当然ファイル全体が大きなひとつのdescribeブロックになっているはず」と信じ込んでいるので、予想に反するネスト構造になっていると脳内の予想と実際の実行結果が異なって混乱します。
以下のように大きなdescribeブロックで囲んで1ファイルにつきトップレベルのdescribeが1つだけになるようにしましょう。
# OK
describe 'My spec' do
describe 'Foo spec' do
# ...
end
describe 'Bar spec' do
# ...
end
end
もしくはFoo specとBar specを別々のファイルに分けるのも一手です。
describe 'Foo spec' do
# ...
end
# Bar specは別ファイルにする
describe 'Bar spec' do
# ...
end
トップレベルにメソッドを定義しない
# NG
def foo
# ...
end
describe 'Foo spec' do
# ...
end
トップレベルにメソッドを定義するとプロジェクトのどこからでもアクセスできてしまいます。
スコープが広いメソッドはどこでどんなトラブルを引き起こすかわからないため、スコープは常に最小限に保つようにしましょう。
# OK
describe 'Foo spec' do
def foo
# ...
end
# ...
end
トップレベルにshared_examplesを定義しない
# NG
shared_examples 'expect something' do
# ...
end
describe 'Foo spec' do
it_behaves_like 'expect something'
# ...
end
トップレベルにshared_examplesを定義するとプロジェクトのどこからでもアクセスできてしまいます。
別の場所で同名のshared_examplesが定義されていたりすると、予期しないタイミングでオーバーライドされる恐れがあります。
shared_examplesはdescribeの内部で定義するようにしてください。
# OK
describe 'Foo spec' do
shared_examples 'expect something' do
# ...
end
it_behaves_like 'expect something'
# ...
end
RSpecのDSLを動的に構築しない
# NG
describe 'Foo spec' do
1.upto(10) do |n|
let!(:"user_#{n}") { FactoryBot.create(:user, email: "user-#{n}@example.com") }
end
# ...
end
RSpecのDSLはテストコードを読む人にとっての共通言語です。
これをループやら条件分岐やらで動的に操作すると、コードの読み手が「DSLが構築される過程」を脳内で再生しなければならなくなり、脳への負担が増えます。
あくまでRSpecのDSLは静的に構築するようにしてください。
# OK
describe 'Foo spec' do
let!(:users) { FactoryBot.create_list(:user, 10) }
# ...
end
なお、上のコード例ではsequenceを使ってemailを生成するようになっている前提です。
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user-#{n}@example.com" }
# ...
end
end
describe/contextの直下に変数や定数を定義しない
# NG
describe 'Foo spec' do
name = 'Alice'
before do
FactoryBot.create(:user, name: name)
end
# ...
end
RSpecのDSLではdescribe/contextの直下に変数や定数を定義することを想定していません。
こうした変数や定数があるとテストコードが読みづらくなったり、予期しない挙動を招いたりする恐れがあります。
RSpecのDSLに沿った形で変数や定数を定義してください。
# OK
describe 'Foo spec' do
before do
name = 'Alice'
FactoryBot.create(:user, name: name)
end
# ...
end
または
# OK
describe 'Foo spec' do
# nameの値をbeforeブロック以外でも使いたい場合
let(:name) { 'Alice' }
before do
FactoryBot.create(:user, name: name)
end
# ...
end
まとめ
というわけで、僕がコードレビューしていて、「ん?いやいや、こんな書き方しちゃダメだよ!」と思ったコード例をまとめてみました。
冒頭に書いたとおり、ネタが増えたらまた追記するので、気になる方はこの記事のストックをお願いします〜!
あわせて読みたい
PR:Railsでテストコードが書けるようになりたいという人へ
DSLがどうこうという話をする前に、そもそもテストコードの書き方に自信がありません!😫 という方へ。
僕が翻訳した電子書籍「Everyday Rails - RSpecによるRailsテスト入門」では、Railsでテストコードを書いたことがないという人に向けて、テストコードの書き方を優しく詳しく説明しています。
RSpecの使い方だけでなく、FactoryBotの使い方も載っています。
まだ読んだことのない方はぜひ一度チェックしてみてください!