Rails tips: ストーリーのあるRSpecテストを書く(翻訳)

概要

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

Rails tips: ストーリーのあるRSpecテストを書く(翻訳)

これまでにRSpecのspyスタブについて学んできましたので、今回はもっと意味のあるspecを作ってみましょう。テストというと、「どう動いているか」をテストすると考えているかもしれません。しかしそのテストを集めてドキュメントをビルドするときのことについて考えていますか?別に余分なことをする必要はありませんが、しなければならないのは「ストーリーが見えるテストを書く」ことだけです。その方法についてご説明いたします。

ストーリーの骨格

よいストーリーにはしっかりした構造があり、だからこそ簡単に追うことができます。ストーリーを追うためにあちこち彷徨わなければならないとしたら、流れを見失ってしまうかもしれません。あなたがテストを読むときにまさにこれがしばしば起きているのです。

設計のまずいテストをちょっと見てみましょう。

require 'spec_helper'

describe NameService do
  let(:name_service) { described_class.new(user) }
  let(:user) { instance_double(User, full_name: 'Mike Willson') }
  let(:admin_policy) { instance_double(AdminPolicy, admin?: false) }

  before do
    allow(AdminPolicy).to receive(:new).with(user).and_return(admin_policy)
  end

  describe '#full_name' do
    subject { name_service.full_name }

    it 'ユーザーのフルネームを返す' do
      expect(subject).to eq(user.full_name)
    end

    context 'ユーザーがadminの場合' do
      before { allow(admin_policy).to receive(:admin?).and_return(true) }

      it 'フルネームの前に`ADMIN`を付けて返す' do
        expect(subject).to eq("ADMIN #{user.full_name}")
      end
    end
  end
end

ここからどんなストーリーが見えてきますか?下手な台本ですね。最初にPolicy Objectを宣言していますが、それがどう使われているのかがわかりません。こんな台本の導入部など読んでられないでしょうから、itタグに進みましょう。

itはたった1行の短いストーリーです。しかしクラスのコードを読んでおかなければ、しばらくの間目を凝らさないと何のメソッドをテストしているのかわかりません(クラスのコードは非常に短いのですが、ここではあえて掲載しませんでした)。

それではもっとましなストーリーを書いてみましょう。

require 'spec_helper'

describe NameService do
  describe '#full_name' do
    it 'ユーザーのフルネームを返す' do
      user = instance_double(User, full_name: 'Mike Willson')
      admin_policy = instance_double(AdminPolicy, admin?: false)
      allow(AdminPolicy).to receive(:new).with(user).and_return(admin_policy)

      name_service = NameService.new(user)
      full_name = user.full_name

      expect(admin_policy).to have_received(:admin?).once
      expect(full_name).to eq(user.full_name)
    end

    it 'ユーザーがadminの場合フルネームの前に`ADMIN`を付けて返す' do
      user = instance_double(User, full_name: 'Mike Willson')
      admin_policy = instance_double(AdminPolicy, admin?: true)
      allow(AdminPolicy).to receive(:new).with(user).and_return(admin_policy)

      name_service = NameService.new(user)
      full_name = user.full_name

      expect(admin_policy).to have_received(:admin?).once
      expect(full_name).to eq("ADMIN #{user.full_name}")
    end
  end
end

今度はずっと読みやすくてよいストーリーだと思いませんか?導入部を除くと2つの章があり(共通のコードを別メソッドに切り出してDRYにすることもできます)。あっちこっち飛び回らずに上から下まですっと読み下すことができ、しかもそれぞれのストーリーがちゃんと独立しています。

よいストーリーを書くには、よい作文スタイルを守らなければなりません。上の例の場合、それぞれのストーリーがセットアップ、操作、検証の3部構成になっています。

1. セットアップ

このサービスで使うクラスをスタブ化し、外部コードを呼び出したりしないようオブジェクトを準備します。テスト対象のクラスで使われるクラスやオブジェクトがどんな値を返すかもわかります。セットアップでは、スタブ化とコードの準備を行います。

2. 操作

ここは最もシンプルな部分です。クラスのインスタンスを作成して、テストしたいコードを呼ぶだけです。

3. 検証

最後の部分では、すべてうまく動いたかどうかをテストしなければなりません。正しい結果が返ったか、正しいクラスが呼び出されたか。

ただしここで忘れてはならないことがあります。上のような戦略は、テストを読みやすくメンテ可能にするためにletbeforeブロックを大量に使うことを強いられる大規模なテストには合いません。

お知らせ: RSpec & TDDの電子書籍を無料でダウンロード

もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。

関連記事

Railsのテスティングピラミッド(翻訳)

テストを不安定にする5つの残念な書き方(翻訳)

デザインも頼めるシステム開発会社をお探しなら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探訪シリーズ