2

Is it possible to use the refinements feature to stub a controller action?

I am defining the refinement in "my_controller_refinement.rb"

require "my_controller"

module MyControllerRefinement
  refine MyController do
    def create_item(my_object = {})
      return "test_id"
    end
  end
end

And using it in the test as follows -

require_relative "my_controller_refinement"

class MyControllerTest < ActionController::TestCase
  using MyControllerRefinement

  test "get item" do
    post :create { my_object: { name: "Test", id: "test_id" } }
    # Post redirects to the show page
    assert_redirected_to action: "show", id: "test_id"
  end
end

The test dir is as -

test/
  -->  my_controller_refinement.rb
  -->  my_controller_test.rb

But the refinement doesnt kick in and the actual controller action seems to get called.

Am I missing something or can refinements not be used for such "stubbing" ?

  • You're probably better off using a [mocking library](https://www.ruby-toolbox.com/categories/mocking) instead. Additionally [ActionController::TestCase is depreciated in Rails 5](http://blog.bigbinary.com/2016/04/19/changes-to-test-controllers-in-rails-5.html) so you might want to rethink and get with the program of integration over controller tests. – max May 18 '17 at 16:13

2 Answers2

2

This won't work because of the way Refinements work currently. The docs (cited below) have the full scoop, but in essence the scoping on a refinement is very narrow.

You may only activate refinements at top-level, not inside any class, module or method scope. You may activate refinements in a string passed to Kernel#eval that is evaluated at top-level. Refinements are active until the end of the file or the end of the eval string, respectively.

Refinements are lexical in scope. When control is transferred outside the scope the refinement is deactivated. This means that if you require or load a file or call a method that is defined outside the current scope the refinement will be deactivated.

Community
  • 1
  • 1
coreyward
  • 77,547
  • 20
  • 137
  • 166
  • Correct me if I am wrong, but from the docs, this is my understanding `c.rb --> my_controller.rb, m.rb --> my_controller_refinement.rb, m_user.rb --> my_controller_test.rb` Are you suggesting I move the `using MyControllerRefinement` outside the class? (I have tried this as well and it doesnt seem to work) – Balachander Ramachandran May 18 '17 at 16:21
  • Or could it be that due to `ActionController::TestCase` the lexical scope is not as expected and hence the refinement can never kick in? – Balachander Ramachandran May 18 '17 at 16:33
  • “Are you suggesting I move the using MyControllerRefinement outside the class?” No, I opened with “this won't work”. I don't know why you thought that may have been suggested. Per the documentation, this isn't going to work. 1) You can't activate a refinement inside of a class, 2) the refinement will be deactivated when you call another method. Since you're not directly calling `@controller.new` from your test (the library is), the refinement won't be active. – coreyward May 18 '17 at 16:56
  • Since the blurb you pasted contained both the top-level activation and lexical scope, I was just trying to understand what was your suggestion. Thank you for clarifying that up! As I commented earlier, I had a hunch that the lexical scope was just not right. Will try to explore that. – Balachander Ramachandran May 18 '17 at 17:18
0

As another answer mentions, refinements are lexically scoped, which means they're only active in the space between the class and end. I can show this with an example:

class OrigClass
  def test_method
    # checks if some methods are defined and calls them if they are
    puts (!! defined? patch_method_1) && patch_method_1
    puts (!! defined? patch_method_2) && patch_method_2
  end
  # the refinement will attempt to overwrite this method
  def patch_method_1; 0; end
end

puts OrigClass.new.test_method # => 0 false

module Refinement
  refine OrigClass do
    # this method is already defined on OrigClass
    def patch_method_1; 1; end
    # this one is not
    def patch_method_2; 2; end
  end
end

# re-open the class to add the refinement
class OrigClass
  using Refinement
  puts new.test_method # => 0 false
  puts new.patch_method_1 # => 1
  puts new.patch_method_2 # => 2
end

puts OrigClass.new.test_method # => 0 false

The final two calls to test_method don't use the refinement methods because of the lexical scoping. This is not using your exact use-case (controller actions) but it is the same concept, and shows that refinements prove difficult to use in this way.

max pleaner
  • 26,189
  • 9
  • 66
  • 118
  • Nice example @max-pleaner, but I believe they are behaving just as they are supposed to! Lets say at the point you re-open the class, you rename the class to say `OrigClassTest` and do `puts OrigClass.new.patch_method_1`, you will see the refinement kicking in. In the context of my example, the reopen of the class to add the refinement corresponds to the test using the refinement. – Balachander Ramachandran May 18 '17 at 16:49
  • Maybe what I am missing is the way tests are generated with the superclass `ActionController::TestCase` and hence I need to rethink the way I am trying to refine the controller action. – Balachander Ramachandran May 18 '17 at 16:51
  • 1
    The thing is, your test case doesn't explicitly call the method you're altering via refinement. It's presumably called behind the scenes somewhere, so the refinement won't activate. – max pleaner May 18 '17 at 17:25