开发者

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

  1. Where are routes meant to be testet in general?
  2. 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 in actionpack
  • gem install gemedit
  • gem edit actionpack
  • Opened lib/action_dispatch/testing/assertions/routing.rb and found recognized_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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜