RSpec testing redirect to URL with GET params

2019-03-22 19:32发布

问题:

Let's say I have a FoosController with a redirect_to_baz method.

class FoosController < ApplicationController

  def redirect_to_baz
    redirect_to 'http://example.com/?foo=1&bar=2&baz=3'
  end

end

I'm testing this with spec/controllers/foos_controller_spec.rb:

require 'spec_helper'

describe FoosController, :type => :controller do

  describe "GET redirect_to_baz" do
    it "redirects to example.com with params" do
      get :redirect_to_baz
      expect(response).to redirect_to "http://example.com/?foo=1&bar=2&baz=3"
    end
  end

end

It works. However, if someone swaps the query string parameters (e.g. http://example.com/?bar=2&baz=3&foo=1), the test fails.

What's the right way of testing this?

I would like to do something like:

expect(response).to redirect_to("http://example.com/", params: { foo: 1, bar: 2, baz: 3 })

I looked at the documentation and I tried searching for response.parameters, but I didn't find anything like that. Even Hash#to_query doesn't seem to solve this problem.

Any help would be greatly appreciated.

回答1:

From the documentation, the expected redirect path can match a regex:

expect(response).to redirect_to %r(\Ahttp://example.com)

To verify the redirect location's query string seems a little bit more convoluted. You have access to the response's location, so you should be able to do this:

response.location
# => http://example.com?foo=1&bar=2&baz=3

You should be able to extract the querystring params like this:

redirect_params = Rack::Utils.parse_query(URI.parse(response.location).query)
# => {"foo"=>"1", "bar"=>"2", "baz"=>"3"}

And from that it should be straightforward to verify that the redirect params are correct:

expect(redirect_params).to eq("foo" => "1", "baz" => "3", "bar" => "2")
# => true

If you have to do this sort of logic more than once though, it would definitely be convenient to wrap it all up into a custom rspec matcher.



回答2:

Are you able to use your route helpers rather than a plain string? If so, you can just pass hash params to a route helper and they will be converted to query string params:

root_url foo: 'bar', baz: 'quux'
=> "http://www.your-root.com/?baz=quux&foo=bar"
expect(response).to redirect_to(root_url(foo: 'bar', baz: 'quux'))

Does that help, or are you restricted to using strings rather than route helpers?

Another thought is that you could just assert directly on the values in the params hash rather than the url + query string, since the query string params will get serialized into the params hash...



回答3:

I had a need for something similar and ended up with this:

Was able to write tests and not worry about query parameter ordering.

expect(response.location).to be_a_similar_url_to("http://example.com/?beta=gamma&alpha=delta")

Drop the following into ./spec/support/matchers/be_a_similar_url_to.rb

RSpec::Matchers.define :be_a_similar_url_to do |expected|
  match do |actual|
    expected_uri = URI.parse(expected)
    actual_uri = URI.parse(actual)

    expect(actual_uri.host).to eql(expected_uri.host)
    expect(actual_uri.port).to eql(expected_uri.port)
    expect(actual_uri.scheme).to eql(expected_uri.scheme)
    expect(Rack::Utils.parse_nested_query(actual_uri.query)).to eql(Rack::Utils.parse_nested_query(expected_uri.query))
    expect(actual_uri.fragment).to eql(expected_uri.fragment)
  end

  # optional
  failure_message do |actual|
    "expected that #{actual} would be a similar URL to #{expected}"
  end

  # optional
  failure_message_when_negated do |actual|
    "expected that #{actual} would not be a similar URL to #{expected}"
  end
end