@scivola

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

URL ヘルパーを含む ActiveDecorator のメソッドがテストで使えない

解決したいこと

ActiveDecorator のデコレーターメソッドに URL ヘルパーやパスヘルパーを使うことがよくあります。

app/decorators/item_decorator.rb
module ItemDecorator
  def path
    item_path(self)
  end
end

上記の path のようなメソッドをテストコードで使うとき,中で使われている item_path が NoMethodError になるので,次節で述べる一工夫が必要です。
その一工夫が,active_decorator 1.4.1 では有効だったのに,1.5.0 以降は効かなくなりました。

1.5.0 以降で通用するやり方が知りたいです。

詳細

コントローラーのテストで,この ItemDecorator#path を使うとします:

test/controllers/items_controller_test.rb
require "test_helper"

class ItemsControllerTest < ActionDispatch::IntegrationTest
  test "decorate" do
    item = Item.create
    ActiveDecorator::Decorator.instance.decorate item

    assert_equal "/items/#{ item.id }", item.path
  end

  # 後略

このままではテストが通りません。
デコレーターで使っている item_url が NoMethodError になります。

1.4.1 までの対処方法

test/test_helper.rb に以下を追加してやります。

test/test_helper.rb
require "active_decorator/view_context"
ActiveDecorator::ViewContext.push Rails.application.routes.url_helpers

ActiveDecorator::ViewContext がどういう働きをしているのか,私は理解していません。
上記のコードはどこかで見て採用したはずですが既に思い出せず,いま探すこともできませんでした。

このやり方が正当なのかどうか分かりませんが,ともかくこれによってテストが通るようになります。

active_decorator 1.4.1 まではこの方法で大丈夫でした。

1.5.0 以降

その後リリースされた 1.5.0, 1.5.1 では再び NoMethodError が出るようになりました。

active_decorator 1.5.0 では ViewContext まわりで 大きな変更が入った ので,そのせいかと思いますが,直接なにが悪いのか分かりませんでした。

もう少し試したこと

ActiveDecorator::ViewContext.push Rails.application.routes.url_helpers

を test_helper.rb ではなく,個々のテストの中に入れて,

test/controllers/items_controller_test.rb
class ItemsControllerTest < ActionDispatch::IntegrationTest
  test "decorate" do
    # ↓ここに追加
    ActiveDecorator::ViewContext.push Rails.application.routes.url_helpers

    item = Item.create
    ActiveDecorator::Decorator.instance.decorate item

    assert_equal "/items/#{ item.id }", item.path
  end

  # 後略

のようにすれば,テストが通るようになりました。
しかし,一箇所に書けば済む方法はないものでしょうか。

各種バージョン

  • ruby 3.4.8 (2025-12-17 revision 995b59f666) +PRISM [arm64-darwin25]
  • Rails 7.2.3
  • minitest 5.27.0
  • active_decorator 1.5.1 と 1.4.1 を対照
0 likes

2Answer

ActiveDecorator::ViewContextActiveSupport::CurrentAttributes を継承するように変更された1ために、コンテキストがリクエストごとにリセットされるようになったのが原因だと思います。

以下のように setup ブロックに書けば ItemsControllerTest 内では1箇所で済みます。

test/controllers/items_controller_test.rb
class ItemsControllerTest < ActionDispatch::IntegrationTest
  setup do
    ActiveDecorator::ViewContext.push Rails.application.routes.url_helpers
  end

  test "decorate" do
    ...
  end
end

コントローラのテストすべてに適用するには以下のように書ける気がします(試してませんが)。

test/test_helper.rb
require "active_decorator/view_context"
ActionDispatch::IntegrationTest.setup do
  ActiveDecorator::ViewContext.push Rails.application.routes.url_helpers
end
  1. このコミット https://github.com/amatsuda/active_decorator/commit/67aaac435af645cdfdfa36e503ffb112dc958b19 。以前は Thread.current にコンテキストを保持していたためテスト中は維持されていた。

1Like

Comments

  1. @scivola

    Questioner

    ありがとうございます!
    「コントローラのテストすべてに適用するには」も期待どおりに動作しました。

    コンテキストがリクエストごとにリセットされるようになったのが原因

    が腑に落ちました。
    だから,setup に書いても,一つのテストで二回リクエストを発生させると,二回目で死にますね。
    (引き続く画面の遷移を見たり,ログインのあとで何かのアクションを見たり)

    基本的に解決しましたので,クローズいたします。ほんとうにありがとうございました。

  2. 一つのテストで二回リクエストを発生させると,二回目で死にますね。

    なるほど。 ActiveDecorator::ViewContext の after reset コールバックチェーンの先頭(逆順に呼ばれる最後のコールバック)1でコンテキストを push してやれば対応できそうな気がします。

    test/test_helper.rb
    require "active_decorator/view_context"
    ActiveDecorator::ViewContext.after_reset prepend: true do
      ActiveDecorator::ViewContext.push Rails.application.routes.url_helpers
    end
    ActiveDecorator::ViewContext.reset
    
    1. コンテキストスタックのリセット処理自体が after reset コールバックとして登録されているためそれより後に実行させたい https://github.com/amatsuda/active_decorator/blob/v1.5.1/lib/active_decorator/view_context.rb#L18-L20

  3. @scivola

    Questioner

    こちら,(ドタバタしているうちに)ご返事するのを忘れていました,すみません。
    after_reset で与えるやり方,うまくいきました。
    これで完全解決です!
    ありがとうございました。

ActiveDecorator::ViewContext.push を使うやり方が正当かどうかについてですが、 ActiveDecorator::ViewContext を Action View の外で使う方法について README で説明されているコードが以下です。

ActiveDecorator::ViewContext.run_with ApplicationController.new.view_context do
  ## perform some heavy jobs here
end

.run_with(arg, &block) はブロックの前後で .push(arg).pop を呼ぶだけなのでほぼ公式の想定通りの使い方だと思います。

1Like

Comments

  1. @scivola

    Questioner

    こちらもありがとうございます。安心しました。

Your answer might help someone💌