Search code examples
capybararspec-rails

Mocking download link / checking link generation against controller methods with 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.


Solution

  • 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