I wrote a gem for Rails that extends ApplicationController with a certain method. This method parses the current URL and uses the result to do a lookup. It looks something like this (simplified):
#current_account = Account.where(subdomain => request.subdomains.first).first
I want to include a test in the gem that asserts that the subdomain is looked up correctly based on a given URL.
I am running into two problems trying to write the test:
1) Since i'm testing within a gem, there is no controller (or Rails app for that matter) so I don't actually know where to start (Unit test, Controller test?)
2) I have searched everywhere, but I cannot find a way to setup the request hash in Rspec for testing. I would expect I would be able to do something like request.url = 'account1.example.com'
Any help on how to setup a proper test for this situation on Rspec is highly appreciated
If you are doing a controller test, then the URLs are usually specified on the configuration, like:
config.action_controller.default_url_options = { host: 'www.test.host' }
that is, if you're testing this as a rails application.
To test that a method in an abstract class works, your best option is to create a Test Subclass, and test using that. Something like
class TestController < ApplicationController; end
and then do your specs around this controller, which should behave exactly as an ApplicationController
EDIT
This would be an example of what I am proposing:
class TestController < ApplicationController
def index
render text: 'fake page' #This is so the action does not fail
end
end
describe TestController do
it 'searches for the current account in the right subdomain' do
Account.should_receive(:where).with({subdomain: 'www'})
get :index
end
end
Related
I have a test class called AdControllerTest, which I am using to test AdController.
From AdControllerTest, I'm trying to call a method defined in AdController, but I don't think I'm doing it right and I can't find the correct way to do this.
My test code looks like so
test "pctr to final list is correct for pctr policy" do
# Make a CTR list
# Make a selectedAds list
# Check that the CTR list reorders the selectedAds appropriately
response = AdCampaign.search query: {
bool: {
must: [
{ match: { target_gender: "F" },
match: { target_country: "KR" } } ]
}}
selectedAds = Array.new(NUMBEROFADS) {Hash.new}
for i in 1..NUMBEROFADS do
selectedAds[i-1] = response.results.to_a[i-1]
end
testCTR = [0.032521635096847835, 0.03863127908388814, 0.007986670179316374]
finalAds = AdController.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
# Manually order selectedAds by testCTR and compare
comparisonAds = Array.new(NUMBEROFADS) {Hash.new}
comparisonAds[0] = selectedAds[1]
comparisonAds[1] = selectedAds[0]
comparisonAds[2] = selectedAds[2]
assert_equal(finalAds, comparisonAds)
end
And within that code I'm trying to call finalAds = AdController.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
The method pctrToAd is definitely defined in AdController.
But I get an error like so:
Error:
AdControllerTest#test_pctr_to_final_list_is_correct_for_pctr_policy:
NoMethodError: undefined method `pctrToAd' for AdController:Class
test/controllers/ad_controller_test.rb:166:in `block in <class:AdControllerTest>'
Am I not supposed to call the method inside a controller that way? If not, how am I supposed to call it?
AdController.pctrToAd is calling a method on the AdController class. Presumably you want to call a method on an AdController object.
NoMethodError: undefined method `pctrToAd' for AdController:Class
^^^^^^^^^^^^^^^^^^
Assuming this is AdControllerTest, the controller object is available as #controller.
finalAds = #controller.pctrToAd(selectedAds: selectedAds, pctr: testCTR)
You don't.
The only methods inside your controller that should be public are the actions of the controller that are called by the router when it responds to HTTP requests.
Those are tested by sending HTTP requests with integration and system tests to your application.
Writing controller tests is a highly flawed and outdated approach that is not recommended by the Rails core team. Isolating controllers is actually very hard as they are Rack applications and have a hard dependency on an incoming request and the Rack middleware stack. Its also not a good idea as the extensive mocking required lets bugs slip through.
If you have a method that you want to call from the outside it does not belong in the controller. Put it in a model, service object or anywhere else where its actually easy to test it isolation. If you don't need to call/test it directly put it in a private method and test it indirectly.
Controllers have tons of responsibilities already. Don't turn them into the junk drawers of your application.
What you do instead.
Extract the logic of the method that needs to be testable in isolation. This can either be into the model layer or a Plain Old Ruby object such as a service object:
# app/services/ad_campains/frobnobizer_service.rb
module AdCampaigns
class FrobnobizerService
def initialize(selected_ads:, pctr:)
#selected_ads = selected_ads
#pctr = pctr
end
def call
# do something amazing
end
def self.call(selected_ads:, pctr:)
new(...).run
end
end
end
require "test_helper"
class AdCampaignsFrobnobizerServiceTest < Minitest::Test
test "fobnobizes the whatchamacallits" do
result = AdCampaigns::FrobnobizerService.call(
selected_ads: [1,2,3], pctr: 'foobarbaz'
)
assert_equals(result[:foo], 'bar')
end
end
When you compare this with a controller its extremely easy to test as it has very few dependencies and moving parts and only does one job.
You then test that the controller calls this component indirectly by sending a HTTP request:
class AdCampaignsController < ApplicationController
# GET /ad_campigns/frobnobize
def frobnobize
#stuff = AdCampaigns::FrobnobizerService.call(
selected_ads: [1,2,3], pctr: 'foobarbaz'
)
# do something with #stuff
end
end
require "test_helper"
class AdCampaignsFlowTest < ActionDispatch::IntegrationTest
test "GET /ad_campigns/frobnobize" do
get '/ad_campigns/frobnobize'
# test that the controller called the service by
# writing assertions/refutions about the headers
# response body or potential side effects.
end
end
In some cases you might want to use spies/mocks to ensure that the controller calls the collaborator as expected instead of testing the outcome.
I am trying to work over a class using Ruby on Rails in order to create a simple controller. In that sense, I have a singleton and I need to refer routes to it. How is it possible?
The message I get:
The action 'foo' could not be found for Test::TestController
The controller file, inside a Test folder:
class Test::TestController < ApplicationController
class << self
def index
render json: {test:"Hello World!"}
end
def foo
render json: {response:"It works!"}
end
end
end
The routes file:
Rails.application.routes.draw do
namespace 'test' do
resources :test
end
get '/:id', to: 'test/test#foo'
end
Its not possible. And its not even a remotely good idea.
Rails controllers take their input in form of the request and env which encompasses things like server settings and anything the middleware has stuffed away as initializer arguments. They are not globals like in for example PHP.
The actions of a controller themselves don't actually take any arguments. So even if you could declare your actions as class methods you would have absolutely no context. And its actually pretty damn irrelevant since you would have to replace the entire router layer to even get it called.
Ruby does not even have real singleton classes either. If you want an object that can't be instantiated use a module (and no you can't make a module a controller in rails).
I am writing controller tests for an application that I did not build, so it's definitely been a learning process. This is my first time encountering a controller that inherits directly from AbstractController::Base. It does not behave, obviously, the same as other controllers.
Its format is roughly:
class SchwadGenericController < AbstractController::Base
def schwad_method var_one, var_two = nil, var_three = nil
if var_two.blank?
var_one.generic_method
end
render template: "schwad_templates/generic_template", layout: false
end
end
I tried normal testing, this is where I am currently at to get ANYTHING to happen.
require 'rails_helper'
describe SchwadGenericController do
# before(:each) do
# SchwadGenericController.skip_authorize_resource
# end
# login_user
let!(:variable){ create(:my_factory_variable) }
describe 'controller methods' do
it 'should hit this method' do
binding.pry
SchwadGenericController.schwad_method(variable)
# expect(response).to_render template: "schwad_templates/generic_template"
end
end
end
And here is roughly where my failures are landing.
Failures:
1) SchwadGenericController controller methods should hit this method
Failure/Error: Unable to find matching line from backtrace
NoMethodError:
undefined method `request=' for # <SchwadGenericController:0x007f8022db0a20>
I read up on abstract controllers and their role in rails here: https://www.mobomo.com/2012/06/and-you-thought-render-farms-were-just-for-pixar/
I read up on the docs here: http://api.rubyonrails.org/classes/AbstractController/Base.html
I would really appreciate another set of eyes on this and guidance as to how you guys have tested controllers and their methods, with controllers that are inheriting from AbstractController::Base.... What am I missing?
-Schwad
After some testing, I don't think this is possible. Controller specs are just wrappers for Rails functional tests which test classes inheriting from ActionController::Base. For controller tests to even run, the controller must support the request and response objects, which is not the case of AbstractController::Base (these are defined in ActionController::Base). That is why you get the particular error when you run the test. For the same reason, you will not be able to use the controller spec helpers (expects) such as to_render because, again, they are defined only for controller specs and your controller class is not a "controller" in the "controller specs" sense.
The only option you seem to have for testing is to test the controller just as any other plain ruby class. You'd need to move your test out of the spec/controllers directory to some other, e.g. spec/abstract_controllers and then you'd have to give up all controller spec helpers and test just calling the instance methods, e.g.:
describe 'controller methods' do
it 'should hit this method' do
c = SchwadGenericController.new
expect(c).to receive(:render).with(template: "schwad_templates/generic_template", layout: false)
c.schwad_method(variable)
end
end
Extending directly from AbstractController::Base seems the likely source of the error to me. Unless you're doing something very nonconventional there should be no reason to do this.
Are you sure you don't intend to inherit from ActionController::Base? There's a whole bunch of modules in ActionController required for rendering which is probably explains the error on a missing method in your tests.
If switching to ActionController::Base doesn't work. Try running app.get "/path/to/action" from the rails console. Do you get the same error?
I use a decorator module that get's included in a model instance (through the "extends" method). So for example :
module Decorator
def foo
end
end
class Model < ActiveRecord::Base
end
class ModelsController < ApplicationController
def bar
#model = Model.find(params[:id])
#model.extend(Decorator)
#model.foo
end
end
Then I would like in the tests to do the following (using Mocha) :
test "bar" do
Model.any_instance.expects(:foo).returns("bar")
get :bar
end
Is this possible somehow, or do you have in mind any other way to get this functionality???
Just an Assumption Note: I will assume that your Decorator foo method returns "bar" which is not shown in the code that you sent. If I do not assume this, then expectations will fail anyway because the method returns nil and not "bar".
Assuming as above, I have tried the whole story as you have it with a bare brand new rails application and I have realized that this cannot be done. This is because the method 'foo' is not attached to class Model when the expects method is called in your test.
I came to this conclusion trying to follow the stack of called methods while in expects. expects calls stubs in Mocha::Central, which calls stubs in Mocha::ClassMethod, which calls *hide_original_method* in Mocha::AnyInstanceMethod. There, *hide_original_method* does not find any method to hide and does nothing. Then Model.foo method is not aliased to the stubbed mocha method, that should be called to implement your mocha expectation, but the actual Model.foo method is called, the one that you dynamically attach to your Model instance inside your controller.
My answer is that it is not possible to do it.
It works (confirmed in a test application with render :text)
I usually include decorators (instead of extending them at runtime) and I avoid any_instance since it's considered bad practice (I mock find instead).
module Decorators
module Test
def foo
"foo"
end
end
end
class MoufesController < ApplicationController
def bar
#moufa = Moufa.first
#moufa.extend(Decorators::Test)
render :text => #moufa.foo
end
end
require 'test_helper'
class MoufesControllerTest < ActionController::TestCase
# Replace this with your real tests.
test "bar" do
m = Moufa.first
Moufa.expects(:find).returns(m)
m.expects(:foo).returns("foobar")
get :bar, {:id => 32}
assert_equal #response.body, "foobar"
end
end
Ok, now I understand. You want to stub out a call to an external service. Interesting that mocha doesn't work with extend this way. Besides what is mentioned above, it seems to be because the stubbed methods are defined on the singleton class, not the module, so don't get mixed in.
Why not something like this?
test "bar" do
Decorator = Module.new{ def foo; 'foo'; end }
get :bar
end
If you'd rather not get the warnings about Decorator already being defined -- which is a hint that there's some coupling going on anyway -- you can inject it:
class ModelsController < ApplicationController
class << self
attr_writer :decorator_class
def decorator_class; #decorator_class ||= Decorator; end
end
def bar
#model = Model.find(params[:id])
#model.extend(self.class.decorator_class)
#model.foo
end
end
which makes the test like:
test "bar" do
dummy = Module.new{ def foo; 'foo'; end }
ModelsController.decorator_class = dummy
get :bar
end
Of course, if you have a more complex situation, with multiple decorators, or decorators defining multiple methods, this may not work for you.
But I think it is better than stubbing the find. You generally don't want to stub your models in an integration test.
One minor change if you want to test the return value of :bar -
test "bar" do
Model.any_instance.expects(:foo).returns("bar")
assert_equal "bar", get(:bar)
end
But if you are just testing that a model instance has the decorator method(s), do you really need to test for that? It seems like you are testing Object#extend in that case.
If you want to test the behavior of #model.foo, you don't need to do that in an integration test - that's the advantage of the decorator, you can then test it in isolation like
x = Object.new.extend(Decorator)
#.... assert something about x.foo ...
Mocking in integration tests is usually a code smell, in my experience.
I must be missing something very simple, but can't find the answer to this. I have a method named foo inside bar_controller. I simply want to call that method from inside a functional test.
Here's my controller:
class BarsController < ApplicationController
def foo
# does stuff
end
end
Here's my functional test:
class BarsControllerTest << ActionController::TestCase
def "test foo" do
# run foo
foo
# assert stuff
end
end
When I run the test I get:
NameError: undefined local variable or method `foo' for #<BarsControllerTest:0x102f2eab0>
All the documentation on functional tests describe how to simulate a http get request to the bar_controller which then runs the method. But I'd just like to run the method without hitting it with an http get or post request. Is that possible?
There must be a reference to the controller object inside the functional test, but I'm still learning ruby and rails so need some help.
I found the answer in "Agile Web Development with Rails" Book. ActionController::TestCase initializes three instance variables needed by every functional test: #controller (contains instance of controller under test), #request, and #response.
You need call this action with HTTP verb like
get :foo
post :foo
etc...