Mocking download link / checking link generation against controller methods with Capybara - capybara

In a Capybara feature spec for a certain page, I have a download link:
download_link = find_link(expected_link_text)
I want to check that the generated link is the correct one to download the file, i.e., that it will call download() on my FileController with the correct model object.
RSpec-Rails seems to have lots of ways to get what I want. In a controller spec, for instance, I could use an ordinary RSpec assertion on the controller:
expect(controller).to receive(:download).with(expected_id)
# download_link = find_link(expected_link_text) # can't do this in a controller spec
# visit(download_link) # can't do this in a controller spec
In a routing spec, I could use route_to():
# download_link = find_link(expected_link_text) # can't do this in a routing spec
expect(get: download_link[href]).to route_to(controller: 'file', action: 'download', id: expected_id)
But in a feature spec, neither controller nor route_to() is available.
With the following shenanigans and a lot of poking around in the debugger, I was able to get route_to() included in my test:
describe 'the page' do
it 'should let the user download a file' do
self.class.send(:include, RSpec::Rails::Matchers::RoutingMatchers) # hack to get routing matchers into feature test
self.class.send(:include, ActionDispatch::Assertions::RoutingAssertions) # RoutingMatchers uses this internally
self.class.send(:define_method, :message) { |msg, _| msg } # RoutingAssertions expects message() to be included from somewhere
#routes = Rails.application.routes # RoutingAssertions needs #routes
download_link = find_link(expected_link_text)
expect(get: download_link[href]).to route_to(controller: 'file', action: 'download', id: expected_id) # works!
end
end
This actually does work, but it's bananas. Isn't there any out-of-the-box way to mix Capybara into other kinds of specs, or mix other kinds of specs into feature specs? Or just a cleaner Rails-y (maybe non-RSpec) way to get the route?
Note: the route isn't named, so I can't use a URL helper (I don't think); and the URL path itself is incoherent noise for historical reasons, so I don't just want to assert the href in string form.

As you stated, if you want to check a specific controller method is being called that would be a controller spec, if you want to verify the route it would be a routing spec. With Capybara you should be writing feature specs/system tests - that means no mocking/stubbing and instead running end-to-end tests. Configure whatever driver you're using to download files, then click the link, download the file, and verify the correct file was downloaded. The other option is to just use url_for rather than trying to include all the extra stuff and just do
expect(download_link[href]).to eq url_for(controller: 'file', action: 'download', id: expected_id)
or better yet
expect(page).to have_link(expected_link_text, href: url_for(controller: 'file', action: 'download', id: expected_id))
But if you're testing file download, you really should just download the file.
If you have to deal with encoding issues you could rewrite the expectation with a filter block and parse both the paths/urls to normalize
expect(page).to have_link(expected_link_text) do |link|
Addressable::URI.parse(link[:href]) == Addressable::URI.parse(url_for(controller: 'file', action: 'download', id: expected_id))
end

Related

Rspec request spec examine response body

Since rspec 3.5 request specs are used to test controller behaviour plus route testing and the correct rendering of the view with the content.
The last part troubles me a bit since i do not understand the thin line of what goes in the view specs to what stays in the request specs.
On relishapp i found this piece:
expect(response.body).to include("Widget was successfully created.")
which tempted me of including the following in my request test:
describe "Index page" do
....
it 'includes a link to cars_parts' do
get "/car_overview"
expect(response.body).to include("car_parts_path")
end
....
end
This test fails.
In the view i use the link_to on the car_parts url_helper.
The failing test dumps the whole response.body String and i see the car_parts_path but rspec does not see it. How do i have to change my test to make it pass without the use of capybara since it is only useable with feature specs now.
And am i doing it correct after all or should this kind of test go somewhere else?
I think you might need to change the string to a method.
Before
expect(response.body).to include("car_parts_path")
After
expect(response.body).to include(car_parts_path) # Remove the quotes
Explanation
car_parts_path is a method in Rails, which will evaluate to the actual path. If you're using that URL helper in your view this should work.
But I thought the response body was a string?
Yup, it is. When you call car_parts_path, you're calling a Ruby method in the Rails framework.
car_parts_path will return a string. This string will be "/car_parts".
Basically, any time you see car_parts_path, imagine it's "/car_parts" as a string. Because the two are exactly equivalent.
In the test
So when you have this in the test:
expect(response.body).to include(car_parts_path) # car_parts_path is a method that returns "/car_parts" so it's the same as...
It's evaluates to the same as:
expect(response.body).to include("/car_parts")
In the view
In your ERB view you'll have:
<%= link_to "Car Parts", car_parts_path %>
This is evaluated to:
<%= link_to "Car Parts", "/car_parts" %>
This renders an a tag in your html:
Car Parts
Conclusion
You're checking for "/car_parts" in the response body and the view contains that string, the test now passes and you're all set!
Why did my original code not work?
Rails was looking for "car_parts_path" as a string - and the view doesn't contain that anywhere.
You were looking for the method name as a string rather than the string the method returns.
Any more questions, just ask!

How to stub paperclip file paths in Capybara/Rspec

For some application I am using Paperclip for file upload (actually the dm-paperclip flavour), and Factory Girl, Rspec, Capybara for testing.
I have a very simple Factory for the "Picture" model, where I am stubbing my file properties as suggested in this post:
FactoryGirl.define do
factory :picture do
title "My Picasso"
description "It's like looking in a mirror."
picture_file_file_name { 'spec/resources/img_1.jpg' }
picture_file_content_type { 'image/jpg' }
picture_file_file_size { 1024 }
end
end
In diverse feature tests with Capybara, I visit pages in which the templates feature thumbnails of the Picture instances:
feature "List of Pictures", :js => true do
scenario "displays appropriately the index page of the pictures with pagination" do
FactoryGirl.create_list(:picture, 21)
visit '/pictures'
# And more testing...
end
end
An example of partial used in one of the templates:
= content_tag_for(:li, picture, :class => 'listed_picture') do
= link_to picture_path(picture) do
- if picture.picture_file?
= image_tag picture.picture_file.url(:thumb)
The problem I have now, is whenever I run the specs, the test fails because there is no matching route for the thumbnail url:
No route matches [GET] "/system/picture_files/1/thumb/img_1.jpg"
Is there any way to stub Paperclip's helper methods to make the test pass?
Thanks in advance for any help!
I just went through this process. Here's how I solved the issue.
First, I created a single method on the object to reference the image URL, both to abide by the law of Demeter and to make for an easier test. For you, that might look like:
#picture.rb
class Picture
...
def picture_file_url(size = nil)
picture_file.url(size)
end
...
end
Now we're ready to stub the Paperclip attachment URL in the spec:
describe "List of Pictures", :js => true do
it "displays appropriately the index page of the pictures with pagination" do
let(:picture) { create(:picture) }
allow(Picture).to receive(:picture_file_url) { "url" }
visit '/pictures'
# And more testing...
end
end
Hope this helps you or someone.

stubbing helpers using mocha

it "should have edit button if user has permission to edit" do
EntitiesHelper.stubs(:permission_to_edit_entity?).returns(true)
get :index
#entities[0..3].each do |entity|
response.should have_selector("form",
:method => "get",
:action => "/entities/edit/#{entity[:id]}") do |form|
form.should have_selector("input", :value => "Edit")
end
end
end
I am trying to write a simple test case which tests that an edit button is showed if the user has permission to edit. I am trying to use stubbing for this. However, it doesn't seem to work. The output view does not show the edit button next to every single entity which I would expect if the stubbing works. I am new to mocha and stubbing - I am doing something wrong here?
Thanks!
I assume EntitiesHelper is a plain-old rails helper that gets mixed into the controller - thus all it's instance methods (such as permission_to_edit_entity?) are available to the controller and views have access to these helper methods (via the controller) ... so You might stub the method on the controller :
controller.stubs(:permission_to_edit_entity?).returns(true)
in this particular case I would even consider changing the stub to mock since You expect the method to be called (although You're testing the button presence, it's good to know that the flow did not happen as expected) :
controller.expects(:permission_to_edit_entity?).returns(true)
but this of course is debatable and You should be fine either way ...

RSpec View testing: How to modify params?

I am trying to test my views with RSpec. The particular view that is causing me troubles changes its appearance depending on a url parameter:
link_to "sort>name", model_path(:sort_by => 'name') which results in http://mydomain/model?sort_by=name
My view then uses this parameter like that:
<% if params[:sort_by] == 'name' %>
<div>Sorted by Name</div>
<% end %>
The RSpec looks like this:
it "should tell the user the attribute for sorting order" do
#Problem: assign params[:sort_for] = 'name'
render "/groups/index.html.erb"
response.should have_tag("div", "Sorted by Name")
end
I would like to test my view (without controller) in RSpec but I can't get this parameter into my params variable. I tried assign in all different flavours:
assign[:params] = {:sort_by => 'name'}
assign[:params][:sort_by] = 'name'
...
no success so far. Every idea is appreciated.
If its a controller test then it would be
controller.stub!(:params).and_return {}
If its a helper test then it would be:
helper.stub!(:params).and_return {}
And its a view test it would be:
view.stub!(:params).and_return {}
If you get warning like below.
Deprecation Warnings:
Using `stub` from rspec-mocks' old `:should` syntax without explicitly enabling the syntax is deprecated. Use the new `:expect` syntax or explicitly enable `:should` instead. Called from /home/akbarbin/Documents/Office/projects/portfolio/spec/views/admin/waste_places/new.html.erb_spec.rb:7:in `block (2 levels) in <top (required)>'.
If you need more of the backtrace for any of these deprecations to
identify where to make the necessary changes, you can configure
`config.raise_errors_for_deprecations!`, and it will turn the
deprecation warnings into errors, giving you the full backtrace.
1 deprecation warning total
Finished in 4.86 seconds (files took 4.72 seconds to load)
You can change it into
allow(view).to receive(:params).and_return({sort_by: 'name'})
That's because you shouldn't be using params in your views.
The best way I see it to use an helper.
<div>Sorted by <%= sorted_by %></div>
And in one of your helper files
def sorted_by
params[:sorted_by].capitalize
end
Then you can test your helpers quite easily (because in helpers tests, you can define the params request.
The easy way is to just do this:
helper.params = {:foo => '1', :bar => '2'}
But in general it's better to be more integration-y and not "stub" values when it's feasible. So I prefer to use controller tests with integrate_views. Then you can specify your params to the get, and test that the entire flow works, from sending params to the controller, to having them processed by the controller, and finally to rendering.
I also generally prefer to pull out view logic into helpers, which can be easier to test.
For instance, say I have a helper called selection_list, which returns a Hash whose "selected_preset" key relies on params[:selected_preset], and defaults to 42 if an empty value is specified for the param.
Here's a controller test where we've called integrate_views (you could of course do the same thing with an actual view test, if you're into that).
describe '#show' do
describe 'selected_preset' do
it 'should default to 42 if no value was entered' do
get :show, :params => {:selected_preset => ''}
response.template.selection_list[:selected_preset].should == 42
This integration test will alert me if some part of this functionality breaks. But I also would ideally like to have some unit tests to help me pinpoint that breakage.
I'll start by having the helper use an instance variable instead of directly accessing params. I'll change the above code by adding a single line directly below the get, as follows:
describe '#show' do
describe 'selected_preset' do
it 'should default to 42 if no value was entered' do
get :show, :params => {:selected_preset => ''}
assigns[:selected_preset].should == 42 # check instance variable is set
response.template.selection_list[:selected_preset].should == 42
Now I also can easily perform a helper unit test:
describe MyHelper do
describe '#selection_list' do
it 'should include the selected preset' do
assigns[:selected_preset] = 3
helper.selection_list[:selected_preset].should == 3
Another method of setting view params:
controller.request.path_parameters[:some_param] = 'a value'

testing REST with shoulda and factory_girl - destroy

i'm developing test for REST using shoulda and factory_girl. Code below
context "on :delete to :destroy" do
setup do
#controller = NewsArticlesController.new
#request = ActionController::TestRequest.new
#response = ActionController::TestResponse.new
#news_article = Factory.create(:news_article)
end
should "destroy new NewsArticle" do
assert_difference('NewsArticle.count', -1) do
delete :destroy, :id => #news_article.id
end
end
should_redirect_to news_articles_path
end
as a result i see
1) Error:
test: on :delete to :destroy should redirect to index. (NewsArticlesControllerTest):
ArgumentError: block not supplied
c:/develop/ruby/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.2/lib/shoulda/action_controller/macros.rb:201:in `instance_eval'
c:/develop/ruby/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.2/lib/shoulda/action_controller/macros.rb:201:in `__bind_1248853182_16800
0'
c:/develop/ruby/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.2/lib/shoulda/context.rb:351:in `call'
c:/develop/ruby/lib/ruby/gems/1.8/gems/thoughtbot-shoulda-2.10.2/lib/shoulda/context.rb:351:in `test: on :delete to :destroy should redirect to index. '
Could you tell me plz - whats wrong and how i can modify test to make them work right?
UPD: routes looks fine
news_articles GET /news(.:format) {:controller=>"news_articles", :action=>"index"}
The problem is with should_redirect_to which now uses block to evaluate the redirect code. Sadly, neither thoughtbot wiki, nor the readme at github reflect this and still contain the old examples.
The correct code is
should_redirect_to "news articles page" { news_articles_path }
where the first argument is just a textual description (it is not eval'd as with the older version) used to generate a test name, so you get a test name like 'should redirect to news articles page'
Maybe you should use a symbol and post method when calling delete:
assert_difference 'Article.count', -1 do
post :delete, :id => ...
end
(referenced from http://api.rubyonrails.org/classes/ActiveSupport/Testing/Assertions.html#M001427)
tkramar solution points in the right direction, but i've had to write the code as:
should_redirect_to("news articles page") { news_articles_path }
Also see the new manual at http://dev.thoughtbot.com/shoulda/classes/Shoulda/ActionController/Macros.html#M000015

Resources