概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: RSpec - 4 common tests design mistakes. Do you make them?
- 原文公開日: 2018/02/13
- 著者: Paweł Dąbrowsk
Rails tips: RSpecのテスト設計でよくあるやらかし4種(翻訳)
私はテストを書くのがとっても好きなのですが、「ヤバいテストを書くぐらいならテストがない方がまし」な状況もあります。テストがなければ全部手動でテストするしかないという覚悟もできようというものですが、ヤバいテストがあるばかりに、自分自身はもちろん関係者まで「問題なし」と誤認してしまいます。
当てにしてはならないヤバいテストかどうかをどうやって突き止めればよいのでしょうか。テスト作成時にやらかしてしまいがちな過ちをいくつかご紹介しましょう。
シンプルなクラスとヤバいテストを1つずつ書いて分析してみることにします。
module Users
class NameService
def initialize(user)
@user = user
end
def name
if user.name.present?
user.name
else
::ExternalAPI.new(user).get_name
end
end
private
attr_reader :user
end
end
ユーザー名がある場合はそれを返し、ない場合は何らかの外部APIを呼び出してオンラインプロファイルからユーザー名を取得するクラスです。続いて、残念きわまるテストを書きましょう。
require 'spec_helper'
describe Users::NameService do
describe '#name' do
it 'nameを返す' do
user = User.create!(name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'nameを返す' do
user = User.create!(name: nil)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
さて、このテストのどこがマズいかおわかりでしょうか?
1. ドキュメントの説明がまったく舌足らず
--format documentation
フラグを付けてspecを実行してみればわかりますが、これではテスト対象のメソッドが何をするのかさっぱりわかりません。知りたかったらクラスやテストコードを開くしかないでしょう。「nameを返す」ほほう、だから何?他の情報が何ひとつありません。テストはアプリにおける極めて重要な情報源なのですから、ペーペーの新人だろうと熟練開発者だろうと、exampleには必ずちゃんと意味の取れることを書きましょう。それでは修正してみます。
describe Users::NameService do
describe '#name' do
it 'ユーザーに名前が1つある場合はユーザー名を返す' do
user = User.create!(name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
user = User.create!(name: nil)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
違いがどれほど大きいかおわかりでしょうか?これなら--format documentation
を付けてspecを実行したときでもいちいちコードを見に行って内容を確認せずに済みます。
2. テストが外部とべったり癒着している
ここでテストしたいのはネームサービスなのに、サービスの外にあるExternalAPI#get_name
クラス内のコードまで呼び出しています。これは単体テストとしておかしいので、コードをもっと細かく分離し(メソッド化するのが普通)、それだけをテストしたいと思います。変更によって他の部分に一切影響を与えたくありません。たとえばクラスは問題なく動作しているがExternalAPI#get_name
の部分でエラーが起きたという状況を考えてみましょう。ここでテストが失敗すると、テストを2つも修正しなければならないでしょう(Users::NameService#name
とExternalAPI#get_name
)。これを避けるためにはスタブを使います。
describe Users::NameService do
describe '#name' do
it 'ユーザーに名前が1つある場合はユーザー名を返す' do
user = User.create!(name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
user = User.create!(name: nil)
external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
これでテストが分離され、しかも前よりずっと高速になりました。たとえAPIクラス内でエラーが発生したとしても、実装は正しいのですからテストは失敗しません。
3. 無意味にデータベースを叩いている
このテストでデータベースを実際に叩く必要はありません。今度もスタブを使ってテストをスピードアップしましょう。
describe Users::NameService do
describe '#name' do
it 'ユーザーに名前が1つある場合はユーザー名を返す' do
user = instance_double(User, name: 'Mike Black')
service = described_class.new(user)
expect(service.name).to eq(user.name)
end
it 'ユーザーが名前を持ってない場合はAPIから取ったユーザー名を返す' do
user = instance_double(User, name: nil)
external_api = instance_double(ExternalAPI, get_name: 'Mike Jr Black')
allow(ExternalAPI).to receive(:new).with(user).and_return(external_api)
service = described_class.new(user)
expect(service.name).to eq('Mike Jr Black')
end
end
end
これでデータベースや外部サービスを叩かなくなりました。テストは分離され、しかもものすごく速くなりました。もちろんほかにもリファクタリングしてUser
モデルインスタンスのスタブを別メソッドに切り出すことも可能は可能ですが、本記事の例では不要です(し、その方がよいのはもちろんです)。
4. privateメソッドをわざわざテストしている
これについてはコード例までは載せませんでしたが、これもよくあるやらかしです。publicメソッドは既にテストしていますし、その結果はprivateメソッドから生み出されたのですから、privateメソッドをテストする必要などありません。どうしてもprivateメソッドをテストしたいというのであれば、まずコードの方をもっと小さなクラスに分割してから、それらを個別にテストすることを検討しましょう。このような事態を避ける最良の方法は、メソッドを設計するときに「単一責任の原則」を用いることです。
お知らせ: RSpec & TDDの電子書籍を無料でダウンロード
もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。