How do I write a Rails 3.1 engine controller test in rspec?
Asked Answered
H

7

45

I have written a Rails 3.1 engine with the namespace Posts. Hence, my controllers are found in app/controllers/posts/, my models in app/models/posts, etc. I can test the models just fine. The spec for one model looks like...

module Posts
  describe Post do
    describe 'Associations' do
      it ...
      end

... and everything works fine.

However, the specs for the controllers do not work. The Rails engine is mounted at /posts, yet the controller is Posts::PostController. Thus, the tests look for the controller route to be posts/posts.

  describe "GET index" do
    it "assigns all posts as @posts" do
      Posts::Post.stub(:all) { [mock_post] }
       get :index
       assigns(:posts).should eq([mock_post])
    end
  end

which yields...

  1) Posts::PostsController GET index assigns all posts as @posts
     Failure/Error: get :index
     ActionController::RoutingError:
     No route matches {:controller=>"posts/posts"}
     # ./spec/controllers/posts/posts_controller_spec.rb:16

I've tried all sorts of tricks in the test app's routes file... :namespace, etc, to no avail.

How do I make this work? It seems like it won't, since the engine puts the controller at /posts, yet the namespacing puts the controller at /posts/posts for the purpose of testing.

Hokkaido answered 5/3, 2011 at 0:12 Comment(0)
E
42

I'm assuming you're testing your engine with a dummy rails app, like the one that would be generated by enginex.

Your engine should be mounted in the dummy app:

In spec/dummy/config/routes.rb:

Dummy::Application.routes.draw do
  mount Posts::Engine => '/posts-prefix'
end

My second assumption is that your engine is isolated:

In lib/posts.rb:

module Posts
  class Engine < Rails::Engine
    isolate_namespace Posts
  end
end

I don't know if these two assumptions are really required, but that is how my own engine is structured.

The workaround is quite simple, instead of this

get :show, :id => 1

use this

get :show, {:id => 1, :use_route => :posts}

The :posts symbol should be the name of your engine and NOT the path where it is mounted.

This works because the get method parameters are passed straight to ActionDispatch::Routing::RouteSet::Generator#initialize (defined here), which in turn uses @named_route to get the correct route from Rack::Mount::RouteSet#generate (see here and here).

Plunging into the rails internals is fun, but quite time consuming, I would not do this every day ;-) .

HTH

Edmundson answered 29/4, 2011 at 13:48 Comment(4)
I wonder if it would be possible to set this as some sort of global somewhere so it doesn't have to be passed to each of the tests...Patriotism
I'd also like to know how you came to know this.Patriotism
@RyanBigg The good old failover once my google-fu hits a wall: reading the source and lots of poking around in the rails console. I spent a full hour on this, because it was my first time reading the routing code.Edmundson
@RyanBigg you can set it as a before block like this: before(:each) { @routes = Posts::Engine.routes }. Found it here: https://mcmap.net/q/374318/-how-to-test-routes-in-a-rails-3-1-mountable-engineKremlin
P
22

I worked around this issue by overriding the get, post, put, and delete methods that are provided, making it so they always pass use_route as a parameter.

I used Benoit's answer as a basis for this. Thanks buddy!

module ControllerHacks
  def get(action, parameters = nil, session = nil, flash = nil)
    process_action(action, parameters, session, flash, "GET")
  end

  # Executes a request simulating POST HTTP method and set/volley the response
  def post(action, parameters = nil, session = nil, flash = nil)
    process_action(action, parameters, session, flash, "POST")
  end

  # Executes a request simulating PUT HTTP method and set/volley the response
  def put(action, parameters = nil, session = nil, flash = nil)
    process_action(action, parameters, session, flash, "PUT")
  end

  # Executes a request simulating DELETE HTTP method and set/volley the response
  def delete(action, parameters = nil, session = nil, flash = nil)
    process_action(action, parameters, session, flash, "DELETE")
  end

  private

  def process_action(action, parameters = nil, session = nil, flash = nil, method = "GET")
    parameters ||= {}
    process(action, parameters.merge!(:use_route => :my_engine), session, flash, method)
  end
end

RSpec.configure do |c|
  c.include ControllerHacks, :type => :controller
end
Patriotism answered 5/10, 2011 at 4:13 Comment(2)
Much better than specifying this every time, bookmarked, thanks!Edmundson
Thank you so much Ryan for this. Works like a charm. Will use this until an even simpler approach comes along.Highwrought
I
19

Use the rspec-rails routes directive:

describe MyEngine::WidgetsController do
  routes { MyEngine::Engine.routes }

  # Specs can use the engine's routes & named URL helpers
  # without any other special code.
end

RSpec Rails 2.14 official docs.

Inhospitable answered 9/7, 2013 at 0:10 Comment(0)
R
5

Based on this answer I chose the following solution:

#spec/spec_helper.rb
RSpec.configure do |config|
 # other code
 config.before(:each) { @routes = UserManager::Engine.routes }
end

The additional benefit is, that you don't need to have the before(:each) block in every controller-spec.

Rhinehart answered 13/3, 2013 at 19:38 Comment(2)
This doesn't seem to work anymore. I had the same idea for a solution but it doesn't seem to make a difference.Categorical
Even if it did work, then you would break all your other tests that are for the rest of your app.Cauliflower
B
2

Solution for a problem when you don't have or cannot use isolate_namespace:

module Posts
  class Engine < Rails::Engine
  end
end

In controller specs, to fix routes:

get :show, {:id => 1, :use_route => :posts_engine}   

Rails adds _engine to your app routes if you don't use isolate_namespace.

Bandanna answered 20/11, 2012 at 15:58 Comment(0)
A
0

I'm developing a gem for my company that provides an API for the applications we're running. We're using Rails 3.0.9 still, with latest Rspec-Rails (2.10.1). I was having a similar issue where I had defined routes like so in my Rails engine gem.

match '/companyname/api_name' => 'CompanyName/ApiName/ControllerName#apimethod'

I was getting an error like

ActionController::RoutingError:
 No route matches {:controller=>"company_name/api_name/controller_name", :action=>"apimethod"}

It turns out I just needed to redefine my route in underscore case so that RSpec could match it.

match '/companyname/api_name' => 'company_name/api_name/controller_name#apimethod'

I guess Rspec controller tests use a reverse lookup based on underscore case, whereas Rails will setup and interpret the route if you define it in camelcase or underscore case.

Andris answered 25/5, 2012 at 18:11 Comment(0)
G
0

It was already mentioned about adding routes { MyEngine::Engine.routes }, although it's possible to specify this for all controller tests:

# spec/support/test_helpers/controller_routes.rb
module TestHelpers
  module ControllerRoutes
    extend ActiveSupport::Concern

    included do
      routes { MyEngine::Engine.routes }
    end

  end
end

and use in rails_helper.rb:

RSpec.configure do |config|
  config.include TestHelpers::ControllerRoutes, type: :controller
end
Garotte answered 8/10, 2020 at 15:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.