【翻訳】そう、コントローラのテストを書くべきなのです!

Rspecでcontroller のテストを書くべきなのかと思うことがあると思います。

Everyday Rails - RSpecによるRailsテスト入門の116ページに以下の記述がありました。

“なぜコントローラをテストするのか? コントローラのメソッドを個別にテストするのにはちゃんとした理由がいくつかあります。 コントローラもメソッドを持ったクラスである。 この点についてはPiotr Solnicaが素晴らしいブログ記事を書いています。そしてRailsアプリケーションにおいて、コントローラはかなり重要なクラス(とメソッド)です。なので、スペック的にモデルと平等に扱うのは良い考えです。”

抜粋:: Aaron Sumner, Junichi Ito (伊藤淳一), AKIMOTO Toshiharu and Shinkou Gyo(魚振江). “Everyday Rails - RSpecによるRailsテスト入門”。 iBooks.

自分のために翻訳してみたので、載せておきます。
原文はこちらにあります。
Yes, You Should Write Controller Tests!

そう、コントローラのテストを書くべきなのです!

コントローラのテストをコーディングすることに意味がないと主張する人がいるのは本当に驚くべきことです。 おそらくもっともありがちなのは、アクションのテストは、ビューが適切に表示されるかのチェックとともに承認テストのなかでカバーされるという主張です。 正しいかといえば、間違っています!遅い承認テストですべての可能性を網羅したコントローラのアクションのシナリオをカバーできるといいはるつもりでしょうか? 例えば、起こるべきすべてのリダイレクトが承認テストの中でテストできると言い張るつもりなのでしょうか? また、承認テストで、すべての無効なリクエストはチェックされているのでしょうか? 存在するコントローラを完全にテストするために、一連の承認テストに27時間13分かけるとでもいうつもりですか? それでは、きっと、あなたの一連の承認テストは多少速くても、おそらくは「都合のよいシナリオ」しかカバーしない、…基本的に、テストがカバーすべき範囲をたくさん外すということになります。

コントローラについての単純な事実

コントローラについて、単純な事実があります。それは複数のメソッドをもつ一つのクラスだということです。もう一度言います。そうなんです、複数のメソッドをもつ一つのクラスなのです。 コントローラーはテストされるべきなのですし、全てのメソッド(アクション)がテストでカバーされるべきなのです。
なぜかといえば、アプリケーションの他のクラスでテストを書くのと同じ理由です。コードが想定通りに動くのか我々は確認したいものです。また、テストをする時には、コントローラーを薄くしておけばずっと楽になります。もし、コントローラ用にテストをコーディングするのが大した作業でないなら、おそらくそれは良いコントローラなのでしょう。 もし、mockのなかで溺れそうになるなら、多分、コントローラをリファクタリングする必要があるでしょう。

ここに、サンプルのcreateアクションをもったユーザーコントローラの例を示します:

class UsersController < ApplicationController
  before_filter :load_group

  rescue_from ActiveRecord::RecordNotFound do
    render :not_found
  end

  def create
    @user = @group.users.create(params[:user])

    if @user.persisted?
      redirect users_path, :notice => ‘User created!’
    else
      render :new
    end
  end

  private

  def load_group
    @group = Group.find(params[:group_id])
  end
end

ご覧のとおり、before_filter においてグループオブジェクトをロードし、そのオブジェクトを使って新しいユーザーを作成します。 非常に、非常に、単純なのです。もし、このコントローラ向けの承認テストをかけと言われても、多分グループが見つからないケースをカバーしたものはできないと断言できます。結局はみんな、普通は、楽観的なシナリオにもとづいて承認テストを書くものです。 もし、すべてのケースをカバーしようとすれば、たぶん納期に間に合わないことでしょう;)

しかし、すべてのpathをカバーすればよいだけではありません!コードの質についてもなのです。 上の例を見てもおそらく、コードのにおいなどしないでしょう。なので、createアクション向けのspecを書いてみましょう:

describe UsersController do
  describe "#create" do
    subject { post :create, :group_id => group_id, :user => attributes }

    let(:group_id) { mock(‘group_id’) }
    let(:group)    { mock(‘group’) }
    let(:user)     { mock(‘user’) }
    let(:users)    { mock(‘users’) }

    before do
      Group.should_receive(:find).with(group_id).and_return(group)
      group.should_receive(:users).and_return(users)
      users.should_receive(:create).with(attributes).and_return(user)
    end

    context ‘when attributes are valid’ do
      it ‘saves the user and redirects to the index page’ do
        user.should_receive(:persisted?).and_return(true)
        subject.should redirect_to(:users)
      end
    end

    context ‘when attributes are not valid’ do
      it ‘saves the user and redirects to the index page’ do
        user.should_receive(:persisted?).and_return(false)
        subject.should render_template(:new)
      end
    end
  end
end

何が起こったのでしょうか?グループオブジェクトに深く入り込み、そのユーザーを取得し、グループオブジェクトを用いて新たなユーザーを生成しているために、 specにはより多くのmockが必要になるし、そうすべきなのです。 ここでの例は、単純なものですが、もしアクションがもっと複雑で、もっと論理が分岐していて、 structural couplingももっとあるなら、specがいったいどんなものになるのかは想像できることでしょう。

簡単にこのアクションをリファクタリングしてみましょう:

class UsersController < ApplicationController
  # stuff

  def create
    @user = @group.create_user(params[:user])

    if @user.persisted?
      redirect users_path, :notice => ‘User created!’
    else
      render :new  
    end
  end

  # more stuff
end

こんどはテストは少し簡単になります:

describe UsersController do
  describe "#create" do
    subject { post :create, :group_id => group_id, :user => attributes }

    let(:group_id) { mock(‘group_id’) }
    let(:group)    { mock(‘group’) }
    let(:user)     { mock(‘user’) }

    before do
      Group.should_receive(:find).with(group_id).and_return(group)
      group.should_receive(:create_user).with(attributes).and_return(users)
    end

    context ‘when attributes are valid’ do
      it ‘saves the user and redirects to the index page’ do
        user.should_receive(:persisted?).and_return(true)
        subject.should redirect_to(:users)
      end
    end

    context ‘when attributes are not valid’ do
      it ‘saves the user and redirects to the index page’ do
        user.should_receive(:persisted?).and_return(false)
        subject.should render_template(:new)
      end
    end
  end
end

グループが見つからないケースもチェックする例も加えるべきです。これはかなりまれなケースではありますが、テストが必要であるという事実に変わりはないのです:

describe UsersController do
  subject { post :create, :group_id => group_id, :user => attributes }

  let(:group_id) { mock(‘group_id’) }
  let(:group)    { mock(‘group’) }

  describe ‘#create’ do
    context ‘when group is not found’ do
      before do
        Group.should_receive(:find).with(group_id).
          and_raise(ActiveRecord::RecordNotFound)
      end

      it { should render_template(:not_found) }
    end
  end
end

これです!小さいテスト、速いテスト。コードがカバーされている。コントローラーも薄い。ここで、ユーザーコントローラ向けのビューのspecも書いてみるのもいいかしれませんが、私は書くつもりはありません、それは面倒です。

総括

やはり、コントローラのためのテストは書くべきなのです。誰かに書くべきかと問われれば、あなたはイエスと答えるべきなのです。コントローラのテストを書かなくてよいとする有効な主張は存在しないのです。 コントローラも、結局はコードであり、テストでカバーされるべきなのです。 テストを設計に組み込めば、コントローラは満足できるものになりますし、さもなければ、滅茶苦茶なものに終わるでしょう。 適切なテストがなければ、コントローラーが、無駄のない、よく設計されたものかどうかを本当の意味で確かめることはできないのです。だから、コントローラ向けのテストを書くべきなのです!

もしまだ見ていないようなら、Avdi の「デメテルの法則」についての素晴らしいポストを読むべきでしょう。この記事でもふれたstructural couplingについて書かれています。

追記(20140408)

一応、元記事の方に掲載の確認?はとってあります。