How can I test arbitrary routes in Rails? - ruby-on-rails

I'd like to write a Rails functional test for a non-RESTful route.
I'm using Test::Unit.
In routes.rb I have this route...
match '/contract_search' => 'contracts#contract_search', \
:as => 'contract_search', \
:via => :post
And in my Contracts controller I have this action...
def contract_search
# ...
end
In contracts_controller_test.rb I tried...
test 'POST to contracts with search params.' do
post(:contract_search, {
:contract_search => {
:title_like => 'System'
}
}, unprivileged_internal_user_session_vars, { })
assert(
assigns(:contracts).length == 6,
"#contracts.length #{assigns(:contracts).length} is incorrect."
)
end
The action works as expected in the browser.
But the test just errors out with...
1) Error:
test_POST_to_contracts_with_search_params.(ContractsControllerTest):
NoMethodError: undefined method `length' for nil:NilClass
test/functional/contracts_controller_test.rb:49:in `block in <class:ContractsControllerTest>'
My sense is that the Test::Unit is trying to post to /contracts/contract_search, I think.
What is the correct way to do this?

Since in your test code you are using assigns(:contracts), You must make sure that your controller method is populating the #contracts variable properly.
May be you have missed some prerequisite to run the test case.

The problem is not with routing, it's with the assigns(:contracts) in your assertion. assigns(:contracts) is nil and so when you call length on it it returns a NoMethodError.
The answer must be in your contract_search action, can you post the code?

Related

Action could not be found in Rspec test for nested route

I am trying to get a handle on how nested routes work with Rspec. I have one of these:
class SupportController < ResourceController
# stuff happens
def support_override
customer = Customer.find_by_id(params[:id])
customer.override( params[:override_key] )
redirect_to("support")
end
end
We have a route:
resources :support do
member do
# loads of paths
get 'support_override/:override_key' => 'support#support_override'
end
end
And the route passes a test:
it "should route GET support/1/support_override/ABCDEF to suport#support_override" do
{ get: '/support/1/support_override/ABCDEF'}.should route_to(controller: 'support', action: 'support_override', id: '1', override_key: 'ABCDEF' )
end
However when I try to test the logic in rspec:
describe SupportController do
# various levels of context and FactoryGirl calls
it "can reach override url" do
get :support_override, { :id=> #customer.id, :override_key="123" }
response.should redirect_to("support")
end
end
I get the following response:
Failure/Error: Unable to find matching line from backtrace
AbstractController::ActionNotFound:
The action 'support_override' could not be found for SupportController
I have no doubt that the problem is with my understanding of how rspec works with nested routes, but I can't see any way to figure out what path Rspec is actually seeking and consequently it's hard to know what I need to change and I'm having trouble locating the relevant documentation.
Is there a way to find what path is being created by the test or can anyone offer guidance on how exactly the path creation works in this situation?
Since, you haven't shared the complete SupportController code, I cannot pin-point exact error. BUT there are two possibilities:
You have defined support_override under private/protected by mistake.
You have closed the class SupportController before support_override method definition, by mistake
Your action must always be public so that its accessible.

RSpec controller testing with non-RESTful route redirects

I'm trying to test some basic aspects of a controller that is reached via a nonstandard set of routes. I can't seem to connect to the appropriate controller/action in my LessonsController, which is reached via redirected routes that are meant to appear to lead to the CoursesController.
When I run my specs, I either get a routing error or the response comes back empty and I'm not sure how to parse it for useful nuggets.
# app/controllers/lessons_controller.rb
def index
... set some instance vars ...
end
# The CoursesController has index and show methods of its own which aren't used here
# app/config/routes.rb
...
get 'courses/:course_name' => redirect('/courses/%{course_name}/lessons'), :as => "course"
get 'courses/:course_name/lessons' => 'lessons#index', :as => "lessons"
...
# spec/controllers/courses_controller_spec.rb
describe CoursesController do
it "test some instance vars" do
get :show, :course_name => Course.first.title_url
assigns(:some_ivar).should_not be_empty
end
end
The error:
AbstractController::ActionNotFound: The action 'course' could not be found for CoursesController
RSpec attempt #2:
# spec/controllers/courses_controller_spec.rb
...
get :course, :course_name => Course.first.title_url
...
The attempt #2 error:
NoMethodError: undefined method `empty?' for nil:NilClass
If I run similar trial-and-error approaches by instead starting with the lessons_controller_spec.rb file (e.g. trying get :index there), I get similar errors. There is no direct route set up for lessons#index, only the redirects.
The response object in my second example is enormous (though the body is empty) so I won't include it unless someone thinks it's useful.
I'm definitely regretting the non-RESTful architecture, but given what it is, is there any idea how to get the controller spec to target the appropriate action inside the LessonsController?
Rails 3.2.12, RSpec 2.14.4, Capybara 2.0.2
Short answer: No.
Actually there are two types of get available in tests.
One type is for controller testing. This get can only accept argument as "action", say :index, :show etc. So you can only use it within current controller test. (Doc here: http://api.rubyonrails.org/classes/ActionController/TestCase/Behavior.html#method-i-get)
The other type is for integration testing. This get can accept any path as argument. http://api.rubyonrails.org/classes/ActionDispatch/Integration/RequestHelpers.html#method-i-get
The two types have same name get but usage is different.
What in your question is controller testing. So you are use the first one. You can only reach actions inside CoursesController. That's why you meet error.
I strongly recommend you to revise the routes right now. It's not about RESTful or not, but your routes break conversion all the day. What's the point the path is lesson, but controller is course? And why you write a Course controller when there is no route for him?

How to test route constraints with rspec

I'm working on an application that will be primarily served as an API (other than a few minor views, such as session/registration, which will be "standard"). I like the approach that was finalized in Railscast #350: Versioning an API, and so followed it. My routes look like:
namespace :api, :defaults => {:format => 'json'} do
scope :module => :v1, :constraints => ApiConstraints.new(:version => 1, :default => false) do
resources :posts, :only => [:create, :show, :destroy, :index]
end
scope :module => :v2, :constraints => ApiConstraints.new(:version => 2, :default => true) do
resources :posts, :only => [:create, :show, :destroy, :index]
end
end
In each route, my Constraint is a new ApiConstraints object, which is located in my ./lib folder. The class looks like this:
class ApiConstraints
def initialize(options)
#version = options[:version]
#default = options[:default]
end
def matches?(req)
#default || req.headers['Accept'].include?("application/vnd.MYAPP.v#{#version}")
end
end
Now, when testing manually, everything works as expected. In my API, I may have between 5 and 10 controllers per version, and don't want to test that the API constraints works for each individual controller, as that makes no sense. I'm looking for one spec file that tests my API constraints, but I'm unsure of where to put that spec.
I've tried adding a spec/routing/api_spec.rb file to test things, but it's not working properly, as it complains that some things aren't provided, like so:
it "should route an unversioned request to the latest version" do
expect(:get => "/api/posts", :format => "json").to route_to(:controller => "api/v1/posts")
end
The above throws an error even though the controller matches properly. It fails with the following error:
The recognized options <{"format"=>"json", "action"=>"index", "controller"=>"api/v1/posts"}>
did not match <{"controller"=>"api/v1/posts"}>,
difference: <{"format"=>"json", "action"=>"index"}>.
Notice that the controller was properly determined, but since I don't want to test for the format and action in this test, it errors out. I would like there to be 3 "API specs":
It should route an unversioned request to the latest version
It should default to the JSON format if none is specified
It should return a specified API version when requested
Does anyone have experience with writing specs for these kinds of routes? I don't want to add specs for every controller inside the API, as they're not responsible for this functionality.
Rspec's route_to matcher delegates to ActionDispatch::Assertions::RoutingAssertions#assert_recognizes
The the argument to route_to is passed in as the expected_options hash (after some pre-processing that allows it to also understand shorthand-style arguments like items#index).
The the hash that you're expecting to match the route_to matcher (i.e., {:get => "/api/posts", :format => "json"}) is not actually a well-formed argument to expect. If you look at the source, you can see that we get the path to match against via
path, query = *verb_to_path_map.values.first.split('?')
The #first is a sure sign that we're expecting a hash with just one key-value pair. So the :format => "json" component is actually just being discarded, and isn't doing anything.
The ActionDispatch assertion expects you to be matching a complete path + verb to a complete set of controller, action, & path parameters. So the rspec matcher is just passing along the limitations of the method it delegates to.
It sounds like rspec's built-in route_to matcher won't do what you want it to. So the next suggestion would be to assume ActionDispatch will do what it is supposed to do, and instead just write specs for your ApiConstraints class.
To do that, I'd first recommend not using the default spec_helper. Corey Haines has a nice gist about how to make a faster spec helper that doesn't spin up the whole rails app. It may not be perfect for your case as-is, but I just thought I'd point it out since you're just instantiating basic ruby objects here and don't really need any rails magic. You could also try requiring ActionDispatch::Request & dependencies if you don't want to stub out the request object like I do here.
That would look something like
spec/lib/api_constraint.rb
require 'active_record_spec_helper'
require_relative '../../lib/api_constraint'
describe ApiConstraint do
describe "#matches?" do
let(:req) { Object.new }
context "default version" do
before :each do
req.stub(:headers).and_return {}
#opts = { :version => nil, :default => true }
end
it "returns true regardless of version number" do
ApiConstraint.new(#opts).should match req
end
end
end
end
...aaand I'll let you figure out exactly how to set up the context/write the expectations for your other tests.

What is going on here: rspec stub(:new).with...?

I'm a little confused about what is going on with the scaffold controller specs that rspec generates. It seemed to be making sense until I added authorization to my app and now I need to update my tests.
MyClass.stub(:new).with('these' => 'params') { mock_my_class(:save => true) }
In my controller I merge a hash into params when creating a new record (it needs the current_user id to be valid). MyClass.new(params[:my_class].merge(:user_id => current_user.id))
Test Fails
expected: ({"these"=>"params"})
got: ({"these"=>"params", "user_id"=>315})
It makes sense that the test fails because the new method receives params it didn't expect. It expected to receive {'these' => 'params'} but it actually received {'these' => 'params', 'user_id' => 1234}
So my natural reaction is to adjust the test because the new method should receive {'these' => 'params', 'user_id' => 1234} and return the mock object.
So I add to the test as follows:
MyClass.stub(:new).with({'these' => 'params', 'user_id' => #user.id}) { mock_my_class(:save => true) }
Here is where I get thrown through a loop. The output of the test is as follows:
expected: ({"these"=>"params", "user_id"=>298})
got: ({"these"=>"params"})
It seems as if a successful test is magically evading me. I'm sure there is a logical reason for these results, but I can't seem to figure them out.
Any help? :)
note:
The rspec site says the following:
Account.should_receive(:find).with("37").and_return(account)
or
Account.stub!(:find).and_return(account)
This is easy enough to follow it just seems odd the the scaffold generated would not contain these methods (unless I botched something which is possible (: )
Passes
login_admin
describe "with valid params" do
it "assigns a newly created forum_sub_topic as #forum_sub_topic" do
ForumSubTopic.stub(:new) { mock_forum_sub_topic(:save => true) }
ForumSubTopic.should_receive(:new).with({"these"=>"params", "user_id"=> #admin.id}) #PASS!
post :create, :forum_sub_topic => {'these' => 'params'}
assigns(:forum_sub_topic).should be(mock_forum_sub_topic) #PASS!
end
end
Fails
login_admin
describe "with valid params" do
it "assigns a newly created forum_sub_topic as #forum_sub_topic" do
ForumSubTopic.stub(:new).with({'these' => 'params', 'user_id' => #user.id}) { mock_forum_sub_topic(:save => true) }
post :create, :forum_sub_topic => {'these' => 'params'}
assigns(:forum_sub_topic).should be(mock_forum_sub_topic)
end
end
"Never trust a junkie", as the saying goes. One could also say, "never trust a scaffold".
OK, that's being a little bit too harsh. The scaffold does its best to figure out which parameters will work for the models/controllers you are generating, but it doesn't know about nested resources (which is what I assume you are using), so it won't generate the user_id in the params hash. Add that:
post :create, :forum_sub_topic => {:user_id=>#user.id}
The these_params key is generated as an example — remove it and add whatever parameters are needed for the controller to create a MyClass.
Regarding the with option: stub and should_receive will only stub out messages that meet the specified conditions, i.e. if you do:
MyClass.stub(:new) {mock_model(MyClass,:save=>true)}
Then MyClass will respond to any new message with the mock. If, on the other hand, you do:
MyClass.stub(:new).with({:bogus=>37}) {mock_model(MyClass,:save=>true)}
Then MyClass will only respond to new when it also receives {:bogus=>37} as an argument.

How can I assert that no route matches in a Rails integration test?

I have a Rails 3 integration test which tests my routes. It contains tests such as:
assert_routing(
"/#{#category.url.path}/#{#foo.url.path}",
{ :controller => 'foo', :action => 'show', :category => #category.to_param, :foo => #foo.to_param }
)
I would also like to test a case where no routes should match. Obviously, testing generation has no meaning in this case, so I just need the inverse of assert_recognizes. I'd like to be able to do something like this:
assert_not_recognized('/adfhkljkdhasjklhjkldfahsjkhdf')
Any ideas, short of wrapping assert_recognizes in an assert_raises block (which is actually not so terrible, now that I think about it)?
There is a similar way for checking this in Rails 4 by asserting on the UrlGenerationError exception:
def test_no_routes_match_when_neither_foo_nor_bar_exist
assert_raises(ActionController::UrlGenerationError) do
get '/category/this-is-neither-a-foo-nor-a-bar'
end
end
I ended up doing this:
def test_no_routes_match_when_neither_foo_nor_bar_exist
assert_raises(ActionController::RoutingError) do
assert_recognizes({}, '/category/this-is-neither-a-foo-nor-a-bar')
end
end
Slightly silly, but it gets the job done.
Note that this does not work with Rails 4. See the answer below for a Rails 4 solution.
By calling #recognize_path you wont receive a false. Instead you'll get an error, but then you found the clue you were looking for.
test "No routes match when neither_foo_nor_ bar exist" do
begin
assert_not(Rails.application.routes.recognize_path(
'category/this-is-neither-a-foo-nor-a-bar'))
rescue ActionController::RoutingError => error
assert error.message.start_with? "No route matches"
end
end
Have you tried method_missing(selector, *args, &block)?
Defined Here

Resources