Is it possible to use ruby refinements to change the behavior of a controller action in tests?

424 Views Asked by At

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" ?

2

There are 2 best solutions below

4
On

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.

3
On

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.