テストの可読性を下げてしまう3つのアンチパターン
はじめに
そもそもテストの可読性が下がるとどうなるのか。何がまずいのか。
筆者が過去に担当したプロジェクトでは下記のようなことが起こりました。
まずテストコードのメンテナンスコストが増加して、新たなロジックにテストが追加されなかったり、CIが落ちた際にテストコードをコメントアウトしてしまったりと、どんどんテストが軽視されるようになってしまいます。
こうなると、テストコードが本来の役割を果たさなくなり、バグが発生しやすくなったり、実装者以外に仕様が伝わりにくかったりと、さまざまな不都合が発生してしまいます。
上記のようにならないために、普段テストコードを書くときに意識している避けるべきことと改善策を、サンプルコードと一緒にまとめてみました。
1. テストデータの名前に連番や意味のない記号を用いる
下のBadの例のようなやつです。let
で宣言したデータに _1
や _a
とネーミングしています。
なぜダメなのかというと、どのようなデータなのかが書いた本人以外には分かりづらいですし、本人も1ヶ月後には分からなくなっているでしょう。
Goodの例に示した _published
や _deleted
のように推測しやすい意味のある名前をつけてあげることで、どのようなデータなのか一目でわかるようになります。
- Bad
let(:article_1) { build(:article, published_at: 3.days.ago) }
let(:article_a) { build(:article, deleted_at: 5.days.ago) }
- Good
let(:article_published) { build(:article, published_at: 3.days.ago) }
let(:article_deleted) { build(:article, deleted_at: 5.days.ago) }
2. データのユースケースをSpecファイルで直接指定している
タイトルが少し分かりづらいですが、Badの例の verified_at: 15.days.ago
が "条件を直接セット" に該当します。
今回のサンプルコードは、ユーザーが申込可能かどうかを判定する applyable?
メソッドのテストです。
例えば、複数箇所で user_applyable
が宣言されていたとします。そこで仮に申込可能の条件に「ユーザーが成人していること」を追加するというような仕様変更があった場合、全ての user_applyable
に対して修正が必要になります。
改善策としてはGoodの例のように、Factoryファイルで trait
を用いてデータのユースケースを用意して、Specファイル側では直接値を指定しない形にすることです。
こうすることで、前述のような前述のような仕様変更の際もFactoryファイルの trait
一箇所を修正すれば良くなっただけではなく、データのユースケースの知識をSpecファイルからFactoryファイルに移行でき、Specファイルの見通しが良くなりました。
- Bad
describe '#applyable?' do
context 'ユーザーが本人確認済みの場合' do
# ↓この記述が複数箇所に存在する
let(:user_applyable) { build(:user, verified_at: 15.days.ago) }
it 'trueを返す' do
expect(user_applyable.applyable?).to eq true
end
end
end
- Good
FactoryBot.define do
factory :user do
# 〜 省略 〜
trait :applyable do
verified_at { 15.days.ago }
end
end
end
describe '#applyable?' do
context 'ユーザーが本人確認済みの場合' do
let(:user_applyable) { build(:user, :applyable) }
it 'trueを返す' do
expect(user_applyable.applyable?).to eq true
end
end
end
let
3. Specファイル冒頭に大量のSpecファイル冒頭で大量の let
でデータを宣言するのも、可読性を損なうのでNGです。
1つのテストケース(context)を読むのに、いちいちファイル冒頭を見に行ったり、使いたい条件に当てはまるデータを探したりするのは、面倒ですし時間がかかります。
Goodの例のように、1つのテストケースに対して必要なデータのみをcontext内で宣言するのが良いです。
- Bad
RSpec.describe User, type: :model do
let(:user_applyable) { build(:user, :applyable) }
let(:user_verified) { build(:user, :verified) }
let(:user_not_verified) { build(:user, :not_verified) }
let(:user_adult) { build(:user, :adult) }
let(:user_declined) { build(:user, :declined) }
let(:user_banned) { build(:user, :banned) }
# 〜 大量のデータの宣言が続く 〜
describe '#declined?' do
context 'ユーザーが退会済みの場合' do
# ↓実際に必要なのは user_declined のみ
it 'trueを返す' do
expect(user_declined.declined?).to eq true
end
end
end
# 〜 省略 〜
end
- Good
RSpec.describe User, type: :model do
describe '#declined?' do
context 'ユーザーが退会済みの場合' do
# ↓テストケースに必要なデータのみを宣言
let(:user_declined) { build(:user, :declined) }
it 'trueを返す' do
expect(user_declined.declined?).to eq true
end
end
end
# 〜 省略 〜
end
まとめ
ここまでで、以下の3つのアンチパターンと改善策を挙げてきました。
- テストデータの名前に連番や意味のない記号を用いる
→ 一目でどんなデータかわかるような命名にする - データのユースケースをSpecファイルで直接指定している
→ Factoryファイルに移行してtrait
を使用する - Specファイル冒頭に大量の
let
→ テストケースごとに必要なデータのみをcontext内で定義する
どれもデータを適切に用意しておくという点で共通しているかと思います。
適切なデータの用意は、テストコードの可読性の向上やメンテナンスのハードルを下げることに役立ちます。
記事の内容は以上です。
Discussion