今日、app/modelsに配置されているただの class で表示用のメソッドがごにょごにょ生えているのを ActiveDecorator に移動させてはどうかと指摘をいただいたので、移動させた。 spec が 1 件だけあったので、これも Decorator のテストに書き換えようとして、ハマった。

まとめ(先に)

  • Thank you, @flada_auxv san!
  • ActiveDecorator のテストをするとき、Controllerインスタンスから view_context を生成すると思うけど、ApplicationControlle#request を設定しておくと、URL使うテストでハマらない。

コード例と現象

まずは、例とするコード(若干適当なところが…)

class GroupSummary
  def initialize(group, total_count, failure_count)
    @group         = group
    @total_count   = total_count.to_i
    @failure_count = failure_count.to_i
  end
end

module GroupSummaryDecorator
  # NOTE これがポイント
  def summary_url
    group_summary_url(@group)
  end

  # こいつらはテストしても期待通りに動く
  def success_count
    @total_count - @failure_count
  end
end

describe GroupSummaryDecorator do
  let(:group) { FactoryGirl.create(:group) }
  let(:group_summary) { GroupSummary.new(group, 0, 0) }

  before do
    ActiveDecorator::ViewContext.push ApplicationController.new.view_context
  end

  after do
    ActiveDecorator::ViewContext.pop
  end

  describe '#summary_url' do
    before do
      ActiveDecorator::Decorator.instance.decorate group_summary
    end

    specify do
      expect(group_summary.summary_url).to eq 'http://localhost:3000/groups/1000'
    end
  end
end

bundle exec rspec spec/decorators/group_summary_decorator_spec.rb
# => NoMethodError: undefined method `group_summary_url` for #<GroupSummary:0x00000000>

ActiveDecorator::ViewContext に ApplicationController インスタンスの view_context を push しているにもかかわらず NoMethodError が出る。 Decorator のメソッドはみつかっているものの、その中の url_for なパスがないって言われる。

調べてみた

ググッてみると、以下の記事をみつけた。

Testing Draper decorators with real helpers, including URL helpers.

hoge_path、fuga_url のような Decorator内で url_for なメソッドを使いたい場合は、view_context のレシーバとなる Controller のインスタンスに request を設定しなければならない様子。

これは ActiveDecoratorが、ここで method_missing を利用して view_context にある同メソッドを呼び出すと思うのですが、ここで actionpack/lib/action_controller/metal/url_for.rb で上書きされている url_optionsurl_for で利用しており、その影響で request が空だとうまく URL が作成できないため(らしい)。

ActionDispatch::TestRequest という便利なものが用意されているので、これを ApplicationController#request に代入してやるとうまいこと URL が作られた。

describe GroupSummaryDecorator do
  let(:group) { FactoryGirl.create(:group) }
  let(:group_summary) { GroupSummary.new(group, 0, 0) }

  before do
    controller = ApplicationController.new
    controller.request = ActionDispatch::TestRequest.new

    ActiveDecorator::ViewContext.push controller.view_context
  end
  ---  ---
end

bundle exec rspec spec/decorators/group_summary_decorator_spec.rb
# => passed

※ URL の port がうまく設定されず困っていたが、このTestRequestのリファレンスみると、’HTTP_HOST’ に対してホスト名とポートをくっつけたものを渡せば反映されるっぽいので、反映したらうまくポートも反映されたのであった。