Rails tips: RSpecのテスト設計でよくあるやらかし4種(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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#nameExternalAPI#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』をどうぞお役立てください。

関連記事

Railsで学ぶSOLID(1): 単一責任の原則(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

ActiveSupport探訪シリーズ