I'm trying to DRY up my RSpec examples by adding a few controller macros for frequently used tests. In this somewhat simplified example, I created a macro that simply tests whether getting the page results in a direct to another page:
def it_should_redirect(method, path)
it "#{method} should redirect to #{path}" do
get method
response.should redirect_to(path)
end
end
I'm trying to call it like so:
context "new user" do
it_should_redirect 'cancel', account_path
end
When I run the test I get an error saying that it doesn't recognize account_path:
undefined local variable or method `account_path' for ... (NameError)
I tried including Rails.application.routes.url_helpers per the guidance given in this SO thread on named routes in RSpec but still receive the same error.
How can I pass a named route as a parameter to a controller macro?
The url helpers included with config.include Rails.application.routes.url_helpers are valid only within examples (blocks set with it or specify). Within example group (context or describe) you cannot use it. Try to use symbols and send instead, something like
# macro should be defined as class method, use def self.method instead of def method
def self.it_should_redirect(method, path)
it "#{method} should redirect to #{path}" do
get method
response.should redirect_to(send(path))
end
end
context "new user" do
it_should_redirect 'cancel', :account_path
end
Don't forget to include url_helpers to config.
Or call the macro inside example:
def should_redirect(method, path)
get method
response.should redirect_to(path)
end
it { should_redirect 'cancel', account_path }
Related
I have an RSpec controller spec and I'm trying to understand how to find what exact route is being called in my example.
In services_controller_spec.rb:
describe 'create comment' do
let!(:service) { FactoryGirl.create(:service) }
describe 'with valid comment' do
it 'creates a new comment' do
expect {
post :add_comment, id: service.id
}.to change(service.service_comments, :count).by(1)
expect(response).to redirect_to(service_path(service))
end
end
end
Is there a way to pp or puts the route that is being sent via the post?
I am asking because I want to post to the route /services/:id/add_comment and want to verify where exactly the route is going.
My routes.rb for this route:
resources :services do
member do
post 'add_comment'
end
end
You can print the name of the route used in an rspec-rails controller spec with something like this:
routes.formatter.send(
:match_route,
nil,
controller: ServicesController.controller_path,
action: 'add_comment', # what you passed to the get method, but a string, not a symbol
id: service.id # the other options that you passed to the get method
) { |route| puts route.name }
rspec-rails uses the route only internally. The above is how rspec-rails (actually ActionController::TestCase, which rspec-rails uses) looks up and uses the route, but with a block that just prints the route.
There are a lot of method calls between the post call in a spec and the above, so if you want to understand how rspec-rails gets to the above I suggest putting a breakpoint in ActionDispatch::Journey::Formatter.match_routes before running your example.
Note that an rspec-rails controller spec doesn't use the route to decide what action method to call or what controller class to call it on; it already knows them from the controller class you pass to describe and the action you pass to the action method (get, post, etc.). However, it does look up the route and use it to format the request environment. Among other uses, it puts the path in request.env['PATH_INFO'].
I investigated this in Rails 4.1, since that's what the project I have handy uses. It might or might not be accurate for other versions of Rails.
I'm trying to write a test following the suggestion on https://stackoverflow.com/a/17002140/4499505.
My simplified test:
test "test" do
log_in_as(#user)
get users_path
assert_template 'users/index'
assigns[:users].each do
assert_select 'a[href=?]', users_path(user)
end
end
The error result:
NoMethodError: undefined method `each' for nil:NilClass
The controller method:
def index
#users_grid = initialize_grid(User.where(verified: true),
per_page: 15,
order: 'users.username',
order_direction: 'desc')
end
Apparantly assigns[:users] is empty even though there are users in the fixtures file. What am I doing wrong? I understand the assigns[:users] should assign the exact same users as shown on users/index, which is exactly what I want.
Well, your correct code is :
test "test" do
log_in_as(#user)
get users_path
assert_template 'users/index'
# in your controller you have the instance var as
# #users_gird, not #users.
assigns[:users_grid].each do
assert_select 'a[href=?]', users_path(#user)
end
end
assigns is a hash, accessible within Rails tests, containing all the instance variables that would be available to a view at this point. It’s also an accessor that allows you to look up an attribute with a symbol (since, historically, the assigns hash’s keys are all strings). In other words, assigns(:contact) is the same as assigns["contact"].
Notice in the answer you've linked to that the symbol in assigns[] matches that of the instance variable from the controller action.
In your case you're assigning the instance variable #users_grid but attempting to iterate through assigns[:users]. You probably just want to change the latter to assigns[:users_grid]
I'm using a custom rspec matcher within a controller spec the message is always empty.
The spec looks like:
describe QuestionnaireController do
matcher :redirect_to_sign_in_if_not_authenticated do |method|
match do |controller|
self.send(method)
response.should redirect_to new_user_session_path
end
end
describe "GET index" do
it { should redirect_to_sign_in_if_not_authenticated(get :index) }
end
end
When running this test, and it fails, all that comes up is:
Failures:
1) QuestionnaireController GET show
As you can see the default should message is missing here. How do I get it to show up?
You can use a failure_message_for_should block, as described here: https://www.relishapp.com/rspec/rspec-expectations/v/2-4/docs/custom-matchers/define-matcher#overriding-the-failure-message-for-should
However, you're probably going to run into a few problems here:
get :index will actually call the get method, and then pass the return value to the matcher, which the code does not seem to be expecting.
Errors & backtraces will probably be messed up if you use another matcher (should redirect_to) inside your custom matcher.
You might want to consider a shared example instead: https://www.relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples
I am trying to create a non ActiveRecord model in my Ruby on Rails application according to http://railscasts.com/episodes/121-non-active-record-model. I am facing hard time testing it though.
I have following class in my app/models/sms.rb file.
class Sms
def initialize
# Do stuff here
end
def deliver
# Do some stuff here
end
end
I am unable to mock deliver method on my Sms class.
it 'should do someting' do
#sms = mock(Sms)
#sms.should_receive(:deliver).and_return(true)
post :create, :user_id => #user.id
flash[:notice].should == "SMS sent successfully."
response.should redirect_to(some_url)
end
In my controller code I do have a line that says #sms.deliver. Yet above gives following error:
Failure/Error: #sms.should_receive(:deliver).and_return(true)
(Mock Sms).deliver(any args)
expected: 1 time
received: 0 times
Any pointers?
Variables beginning with # are instance variables. The #sms your controller refers to is not the same #sms as your spec has defined.
Try changing
#sms = mock(Sms)
#sms.should_receive(:deliver).and_return(true)
to
Sms.any_instance.should_receive(:deliver).and_return(true)
If your version of RSpec doesn't have the any_instance method, you'll need to stub :new on the class:
#sms = mock(Sms)
Sms.stub(:new).and_return(#sms)
#sms.should_receive(:deliver).and_return(true)
I have a Rails controller where I accidentally defined the 'edit' method inside the 'create' method.
My Controller with the error:
class UsersController < ApplicationController
...
def create
#user = User.new(params[:user])
...
def edit
#user = User.find(params[:id])
#title = "Edit user"
#check = "BORK" # something I added for testing the rendered output
end
end
end
An example test;
it "should have the right title" do
get :edit, :id => #user
response.should have_selector('title', :content => 'Edit user')
end
So when I run the tests (I use rspec) and output the response.body, the User edit.html.erb template is rendered correctly; all the instance variables are visible. So all the tests pass.
Visiting the 'edit' URL correctly shows an error; the template uses #user instance variable, and it's not set correctly. Of course correcting the controller fixes the error.
I don't understand why the tests pass at all and why, in the test, all the instance variable values are visible?
My instinct suggests this is a scope problem? Something about #user being an instance variable, and that in the tests it's set within the scope of the test, but in my controller it's within the scope of the inner 'edit' method? But how does the test even find the 'edit' method? In what scope does that inner 'edit' method exist?
You should realise that the def construct is as much executable code as an if statement. It's not invalid to put it inside another method, but it won't be run until the outer method is called:
>> class Foo
>> def foo
>> def bar
>> end
>> end
>> end
=> nil
>> Foo.instance_methods(false)
=> ["foo"]
>> Foo.new.foo
=> nil
>> Foo.instance_methods(false)
=> ["foo", "bar"]
The reason this was erroring in your browser was because Rails reloads all (most) of your classes each request. So, even if you had visited the create action - which would cause the edit method to be defined - the following request would have unloaded it again.
However in the test environment, if an earlier test had called the create action then that would have defined the edit action for future tests. You would see a different result if your tests were run in a different order (which in itself makes it a bad idea to rely on this).
Generally of course this isn't what you want at all, so just clear it up and move along :)