Testing subdomain constrained routes in Rails 3
I'm testing my Rails applications with Test::Unit. A problem I often come across is testing my application's routes for which I haven't found a solution yet.
At the moment, I'm working on an application which uses Basecamp-style subdomains to differentiate accounts.
There are routes which require a subdomain
constraints(SubdomainRoute) do
get "/login" => "user_sessions#new", :as => :login
get "/logout" => "user_sessions#destroy", :as => :logout
...
end
as well as routes which can be accessed only without a subdomain
constraints(NoSubdomainRoute) do
match "/" => "public#index", :as => :public_root
match "/signup" => "public#signup", :as => :signup
...
end
The class SubdomainRoute is defined as:
class SubdomainRoute
def self.matche开发者_运维技巧s?(request)
request.subdomain.present? && request.subdomain != "api" && request.subdomain != "www"
end
end
The class NoSubdomainRoute pretty much does the opposite.
Routing works as expected, but how do I test those using Test::Unit?
In functional tests, I can do something like
assert_routing "/signup", :controller => "public", :action => "signup"
but I cannot provide a subdomain, so in fact that's only testing Rails internals which adds nothing to testing my application. What I want to test in this case is wheter signup_path/signup_url is accessible with or without a subdomain.
In code, something like this
assert_raise(ActionDispatch::RoutingError) { get "http://account.test.host/signup" }
get "http://www.test.host/signup"
assert_response :success
get does not work in this case because Test::Unit treats the whole URL as the controller's action (... :action => "http://account.test.host/signup").
Setting
@request.host = "subdomain.test.host"
only has influences on your inner controller code (e.g. getting current account by extracting subdomain from host) but does not affect routing in this case.
I don't know if the situation is any different in integration tests.
So the two main questions are
- Where are routes meant to be testet in general?
- How are they tested regarding this special case?
I'm williing to try out different approaches (Capybara and friends) but I don't want to switch my testing framework (already had my RSpec-time) since I'm otherwise very happy with Test::Unit.
Thanks in advance and kind regards!
Actually the solution is very simple as assert_routing
does support URLs:
assert_routing "http://subdomain.example.com/login",
{ :controller => "user_sessions", :action => "new" }
assert_routing "http://www.example.com/signup",
{ :controller => "public", :action => "signup" }
The ability to provide a URL to assert_routing was added here.
Search strategy:
- Found
assert_routing
was defined inactionpack
gem install gemedit
gem edit actionpack
- Opened
lib/action_dispatch/testing/assertions/routing.rb
and foundrecognized_request_for
is what handles the path processing - Opened the file via GitHub, and selected 'Blame' to see which commit added that functionality
I've found rr to be the best way to test subdomains and custom domains. For example I have a rack app which manipulates the custom domain. Here is a sample test:
require File.join(File.dirname(__FILE__), '..', 'test_helper')
require 'rr'
require 'custom_domain'
require 'rack/test'
class CustomDomainTest < ActiveSupport::TestCase
include Rack::Test::Methods
include RR::Adapters::TestUnit
def app
Rails.application
end
def test_cname_to_subdomain
mock(CustomDomain::Cname).resolver('www.example.com', '.lvh.me') { 'subdomain.lvh.me' }
get 'http://www.example.com:80/users'
assert_equal 'www.example.com, subdomain.lvh.me:80', last_request.env['HTTP_X_FORWARDED_HOST']
assert_equal 'www.example.com', last_request.env['SERVER_NAME']
assert_equal 'www.example.com', last_request.env['X_CUSTOM_CNAME']
assert_equal 'subdomain.lvh.me', last_request.env['X_CUSTOM_SUBDOMAIN']
end
end
end
And here are a few links discussing this topic you might find useful:
- http://www.brynary.com/2009/3/5/rack-test-released-a-simple-testing-api-for-rack-based-frameworks-and-apps
- http://effectif.com/articles/testing-rails-with-rack-test
- http://gitrdoc.com/brynary/rack-test/tree/master
- http://github.com/brynary/rack-test
- http://jasonseifer.com/2009/04/08/32-rack-resources-to-get-you-started
- http://guides.rubyonrails.org/rails_on_rack.html
- http://rack.rubyforge.org/doc/SPEC.html
Good luck.
I looked for non-awful ways to patch into the route testing machinery and didn't find any.
I ended up just stubbing out the constraints' match? methods:
describe ThingsController do
shared_examples_for "a subdomain route" do |http_method, path, expected_action|
context("on a subdomain") do
before do
stub(SubdomainRoute).matches? { true }
stub(NoSubdomainRoute).matches? { false }
end
it { should route(http_method, path).to(:action => expected_action) }
end
context("on the main domain") do
before do
stub(SubdomainRoute).matches? { false }
stub(NoSubdomainRoute).matches? { true }
end
it { should_not route(http_method, path).to(:action => expected_action) }
end
end
it_should_behave_like "a subdomain route", :get, '/things/new', :new
...
(I am using rspec and rr. I like to put route tests in my controller specs, just before the describe blocks for the actions. Shared examples will be moved to a module that gets mixed into controller specs.)
In order to make my constraints run in development mode, I add a value to the ENV hash, starting the server like so:
site=trivial.ly ruby script/rails server
Here I'm passing the entire domain, you could instead pass a subdomain. Then in my constraint class I detect either the domain, or the ENV[:site] value, like so:
class DomainConstraint
def initialize(domain)
@domains = [domain].flatten
end
def matches?(request)
@domains.include?(request.domain) || @domains.include?(ENV["site"])
end
end
Testing a controller with a particular constraint is now simply a matter of setting the correct ENV[:site] value like so (here in test::unit):
require 'test_helper'
class TrivialLy::SplashPagesControllerTest < ActionController::TestCase
test "should get android splash page" do
ENV["site"] = "trivial.ly"
get :android
assert_response :success
end
end
This works for domain contraints, it would work equally well for subdomain constraints.
None of these other answers answer the question, at least not in any elegant way. Mocks aren't needed.
To use Rails 3 subdomain constraints in a Rails integration test: simply include the domain as part of the request in your integration test:
get "http://admin.example.com/dashboard"
I tested this successfully on (pretty much) the following route, with no problems:
scope :admin, as: 'admin', module: 'admin' do
constraints subdomain: 'admin' do
resource 'dashboard', controller: 'dashboard'
end
end
Perhaps the asker wasn't using integration tests, or an older version of Rails.
I would test the functionality of your class not necessarily the route itself. I feel that is essentially testing rails.
You know that if you pass your class to the constraints block it will work if you've defined the .matches? method. So just test that you are getting the expected logic there.
As matt Polito said above, test what you want to happen.
In your constraints, you must have some kind of before_filter or something that handles the case when someone isn't supposed to be where they are. or even like www.yourdomain.com/login
you must handle this situation with a redirect and a flash warning, so you can test against that.
even still, in my case for subdomains, i assigned a variable in a mailer with the subdomain. It was that variable that i checked in my test, such as:
setup do
# get_sub is just @request.host = "#{sub}.local.me" in test_helper.rb
get_sub("two")
@deal = deals(:two)
end
test "should get show" do
get :show, :token => @deal.token
assert_response :success
["deal", "subdomain", "user_profile"].each do |variable|
assert assigns[variable.to_sym], "I can't find a var called #{variable}"
end
end
so testing that whatever i want works, and is passing the sub. in your case it should just be that /login responds with :success, i think.
Subdomains in rails 3 is very simple it is just putted as :
constraints :subdomain => subdomain_name do
#here comes all routes which routes under above subdomain
end
精彩评论