How to mock a Rails subclass contant in Rspec? - ruby-on-rails

I have a BaseController that I want to superclass other controllers with. I also superclass some controllers with the standard ApplicationController.
For example, I may have:
class AController < ApplicationController
end
as well as...
class BController < BaseController
end
When testing, I sometimes need to create an arbitrary "mock" or "fake" class to test before_filters, module includes, etc. I don't do this often, but on occasion it's helpful.
I created one in my specs like so:
class FakeController < ApplicationController
end
That's fine.
But now, I need to create another one of these FakeController classes, but this time as a subclass of my BaseController class.
Unfortunately the FakeController constant is already registered and I'm getting superclass mismatch errors.
I don't want to do something like FakeController2 -- as I think this opens a bad can of worms.
Any suggestions?

I don't see the issue with using FakeController2 or any other name, but the only public alternative I can think of is to introduce a module so that your second FakeController exists in a different namespace, as in:
module Foo
class FakeController < ApplicationController
end
end
There is a private method remove_const defined on Kernel which can be used to unregister a constant from an object. So, if FakeController is defined on Object, you can unregister it with the call:
Object.send(:remove_const, :FakeController)
At that point, you can define the constant again as you would if it had never never been defined in the first place. (Remember: Ruby is an interpreted language.)

An alternative to subclassing your controllers is to use the anonymous controller mechanism provided by the rspec-rails gem.

Related

How rails(ActiveRecord) defines the class method on models and how to remove one of them if needed

I am trying to figure this out on my journey to rails and ruby's deeper understanding.
What i know is that we can check if a method is defined in a ruby class by calling: method_defined? on the object. As all classes are also objects of class Class we can do same for class methods. For example if class Foo defines bar class method, this is what happens:
Foo.method_defined? :bar #-> true
But when applying same on models which are inherited from ActiveRecord::Base(directly or indirectly). this results in:
User.method_defined? :all #-> false
User.method_defined? :count #-> false
I can see the all method defined here, i am struggling to match the dots and make sense of whats going on. And how these methods work on models when they are not implemented as class methods neither is there any funky business of method_missing is going on(as it seems).
While on it, if i can get same explanation for instance methods which rails adds for model objects, like name method in User.first.name(assuming user table has name column). Would be a plus.
Lastly, some word on how to remove one of these methods if we ever need to.
EXTRA: If i can also get to know how to reset the User class to have the method defined again after removing, like if i remove count method with the suggestion from comments: User.instance_eval { undef :count } i also want to be able to redefine this back. Or kind of reset the User class. load 'app/models/user.rb' does not do the job here.
Update: I figured out how to reset the class after undefining a method in the eigenclass of model User before doing load 'app/models/user.rb' i had to explicitly do Object.send(:remove_const, :User) so ruby removes the class entirely than do the load thing.
Still struggling to digest all this and especially how the rails implementation works.
No magic here
module X
def test
end
end
class A
extend X
end
class B
include X
end
A.method_defined? :test #false
B.method_defined? :test #true
so it's not defined because it's a class method and class methods are defined in the singleton class.
method_defined? check if the method is defined in the class or its ancestors only.
B.ancestors #[B, X, Object, Kernel, BasicObject]
A.ancestors #[A, Object, Kernel, BasicObject]
so simply because it's a class method
UPDATE: Adding more trace to How all is defined
the method all is defined as mentioned in https://github.com/rails/rails/blob/b9ca94caea2ca6a6cc09abaffaad67b447134079/activerecord/lib/active_record/scoping/named.rb
this module extends ActiveSupport::Concern which mean if you included this module the methods in ClassMethods will be added as class methods to the includer (more about this https://api.rubyonrails.org/classes/ActiveSupport/Concern.html)
in the active record entry point here https://github.com/rails/rails/blob/b9ca94caea2ca6a6cc09abaffaad67b447134079/activerecord/lib/active_record.rb#L151 the module Named is autoloaded inside Scoping module which resulted in having module called ActiveRecord::Scoping::Named the module mentioned above
here in the base class, the Scoping module is included https://github.com/rails/rails/blob/b9ca94caea2ca6a6cc09abaffaad67b447134079/activerecord/lib/active_record/base.rb#L298 which define all as class method
so it's similar to the simple code above but using some of ActiveSupport magic like autoloading , egarloading and concerns.
We can remove a method using
User.instance_eval { undef :count }
and can redefine if its in parent class using
User.instance_eval do
def count
super
end
end
Hope it would help you
Not sure, but. Method method_defined? cares about instance public methods. Instead, if you want to check is any object responds to a method (as a class it's also object) use respond_to?.

Reason for calling an instance method on a class in Ruby?

I would like to know is there any specific reason for doing this or is this a silly mistake done by someone (or is it something else that I am not understanding).
class SomeMailer < ActionMailer::Base
def first_method(user)
mail(to: user.email, subject: "Testing")
end
end
This method is called at some other place in the code as follows
SomeMailer.first_method(user).deliver
ActionMailer::Base classes are weird... Yes, you do indeed call instance methods on the class - which obviously won't work for 'normal' classes!
But there's some meta-programming magic under the hood:
module ActionMailer
class Base < AbstractController::Base
def method_missing(method_name, *args) # :nodoc:
if action_methods.include?(method_name.to_s)
MessageDelivery.new(self, method_name, *args)
else
super
end
end
end
end
If you look through the rails documentation, you'll see that calling instance methods on the class is, strangely, the normal thing to do for mailers.
This is how rails is intended to work.
It is also mention in rails guides that
You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
Rails do the internal processing by invoking method_missin.
Basically, any action method defined in mailer class will be intercepted by method_missing and will return an instance of MessageDelivery, otherwise it runs the default implementation. And where do action methods come from? ActionMailer::Base inherits from AbstractController::Base, so it works exactly the same as for controllers - it returns a set of public instance methods of a given class.
Rails itself encourage this behavior. For more information, you can refer this link
I will try to answer this since I have come across similar situations myself while working on existing code.
The instance methods like this in a class help when you do call backs on a class. For example, if you want to perform some action on a object that was created from that class.
Say you have another class User and you want to send an email to a user immediately after creating a new user. In that case you can call this method on that object by doing
after_save :method_name

Can a ruby class method inherit from another class?

I read here that a ruby class can only inherit from one class, and can include modules.
However, the devise module defines controllers like this:
class Users::PasswordsController < Devise::PasswordsController
...
end
Now, given that Users is probably a class, with PasswordsController being a method:
>> Devise::PasswordsController.class
=> Class
How is it that a method in a class inherits from another class?
class Users::PasswordsController < Devise::PasswordsController
...
end
In the above code, Users is the module and PasswordsController is the class inside Users module. Similarly Devise is the module and PasswordsController is the class inside Devise module.
so when you run
Users::PasswordsController.class
#=> Class
Users.class
#=>Module
What confuses you here is that you have wrong assumptions, namely:
Users is probably a class
Not necessarily. Here we have namespace with nesting, therefore Users can be either a class or a module. In fact classes are modules.
PasswordsController being a method
PasswordsController here is a class nested in the Users namespace. :: simply lets you go one level into the nesting tree.
Consider:
module Foo
class Bar
end
end
Foo::Bar.class # => class
From Rails naming convention, Users is most probably a module, and Users::PasswordsController is a class.
Note that :: is not for calling class methods (although it can be used this way). It's for accessing constants inside a module/class. For example
module Foo
BAR = 'bar'
end
Foo::BAR
#=> "bar"
In Ruby, a module/class name is a constant, and the value stored in it is the module/class. So :: also is used for accessing a module/class inside another module/class. For example
module Foo
class Bar
end
end
Foo::Bar
#=> Foo::Bar
Both Users and Device are modules, just used for scoping the real classes that are PasswordsController and PasswordsController.

Add rails route helpers to a class as class methods

How can i add rails route helpers like "root_path" to a class like my_model.rb as a class method?
So my class is like this:
Class MyModel
def self.foo
return self.root_path
end
end
MyModel.foo
The above doesn't work because Class MyModel doesn't respond to root_path
This is what I know:
I can use include Rails.application.routes.url_helpers, but that only add the module's methods as instance methods
I tried doing extend Rails.application.routes.url_helpers but it didn't work
Please feel free to school me :)
URL routes shouldn't generally need to be accessed from a model. Typically you should only need to access them from your controller when handling a request, or when rendering a view (if you're e.g. formatting a link URL).
So instead of asking your model object for the root path, you would simply call root_path from within your controller or a view.
Edit
If you're just interested in the reason why you're unable to include the module's method as class methods in your class, I would not expect a simple include to work, since that would include the module's methods as as instance methods in your class.
extend would normally work, but in this case it does not due to how the url_helpers method is implemented. From actionpack/lib/action_dispatch/routing/route_set.rb source
def url_helpers
#url_helpers ||= begin
routes = self
helpers = Module.new do
...
included do
routes.install_helpers(self)
singleton_class.send(:redefine_method, :_routes) { routes }
end
The included block containing the routes.install_helpers(self) call indicates that you will need to include the module in order to get the methods install (so extend is out).
The following should work if you call extend in the class context. Try this:
Class MyModel
class << self
include Rails.application.routes.url_helpers
end
end
Class.root_path

Call a controller's method in other controllers (staying DRY)

I'm slightly new to Rails (i.e. stupid and need some teachin').
I have a controller (call it ControllerFoo) that performs a particular task (theMethod) which could be useful in other controllers (say, from within ControllerBar). So, of course, the method is defined as self.theMethod in ControllerFoo (which means it's a class method, right?), and access in ControllerBar as ControllerFoo.theMethod. Confused yet?
Here's the problem: the ControllerFoo.theMethod uses session data, and when called from ControllerBar, session is nil. In fact, it seems that session is also nil when being called from itself. I guess what I'm saying is class methods can't access session data?
<rant>I hate how session data can't simply be accessed anywhere like in PHP</rant>
So for now, since I'm not smart enough to know how to do this correctly, I've just duplicated the logic in several places throughout my app. But this is not DRY at all, and I hate it.
So how can I create a method in a controller that's accessible to other controllers and can also access session data?
class ControllerFoo < ApplicationController
def self.theMethod (greeting)
p "#{greeting} #{session[:user]}!"
end
end
class ControllerBar < ApplicationController
def show
ControllerFoo.theMethod("Hello,")
end
end
Couple of options...
Put the shared method in the shared parent ApplicationController
Create a module that both ControllerFoo and ControllerBar will include
e.g.
module SharedModule
def theMethod (greeting)
p "#{greeting} #{session[:user]}!"
end
end
class ControllerFoo < ApplicationController
include SharedModule
end
class ControllerBar < ApplicationController
include SharedModule
def show
theMethod("Hello,")
end
end
The way you would do this is Ruby would be to create a module containing the class (or instance) methods you wish to share and include it in the classes you need to have those methods defined in.

Resources