I gave a lightning talk at Railsconf in response to a talk done by Andy Maleh about engines. His talk contained some content that I strongly disagreed with, and so I felt it necessary to set the record straight. You can see the slides for it here. I will have a more thorough dissection of the talk once the video is online.
To be perfectly honest, I am probably bitter because I was going to submit a talk about engines and then decided to submit another talk instead which ultimately didn’t get accepted. Nevertheless…
Drawing routes
The first thing that I can recall disagreeing with is Andy’s suggestion to draw routes for the engine on the Rails.application.routes
object, by putting
this code inside the config/routes.rb
file of the engine:
Rails.application.routes.draw do
resources :people
end
The problem with this is that if the application’s routes file has a catch-all route at the bottom, the route for the engine won’t get matched because it is drawn after the application.
This leads to additional complexity as well, as by drawing these routes on the application like this, they would also correspond to a controller that’s at the
top-level of the namespace, i.e. PeopleController
. This can lead to confusion as to where the controller is located. How can someone know if PeopleController
is inside the engine or is inside the application? Furthermore, the more engines that get added to this application, the more confusing such a thing would get.
By namespacing the routes properly (as we do in Spree and Forem) , like this:
Spree::Core::Engine.routes.draw do
resources :products
end
And by using isolate_namespace
inside your engine, the routes will be a completely separate entity for each engine, rather than one big “ball o’ mud”. In
Spree this namespace isolation is to the Spree
module (i.e. isolate_namespace Spree
), so that the controllers will be Spree::ProductsController
and not
Spree::Core::ProductsController
and the like.
By using a completely isolated namespace like this, your controller would end up being correctly namespaced and then there’s no confusion as to which engine
the controller is located within. You can then mount your engine at whatever path you want inside your application by explicitly defining the mount point
inside config/application.rb
like this:
mount Spree::Core::Engine, :at => "/"
By mounting it at root, it will seem as if the engine were a part of the application itself. If you wanted to change where the engine was mounted, it’s a
simple matter of changing the :at
: option and you can carry on like normal.
There’s a whole bunch of other stuff that isolate_namespace
does as well, like namespacing of models and tables. It’s absolutely important that you namespace
your engine to avoid confusion.
Oh, and one more thing: by doing this namespacing, if you want to get to an engine route from inside the application, you’ll need to call the engine’s routing proxy method, like this:
spree.products_path
If you attempted to do products_path
it would go to the application’s products_path
, which may be undefined.
Similarly, to get to the application’s routes from inside the engine, use main_app
:
main_app.people_path
Switching back and forth
The second thing that I disagreed with during Andy’s talk was that he said that when developing an engine you needed to constantly switch back and forth between the application and the engine. This is simply not true. If you are developing your engine correctly, the development of it can be done in complete isolation from a real application and instead depend on a dummy application. It should not be a requirement for you to have two separate projects coupled together like this so you can test one or the other.
How we do it in Forem is that we have a dummy application inside spec/dummy
which is generated by running bundle exec rake forem:dummy_app
. This
command creates a new dummy application with things like
User
model already set up inside it. Inside Spree, there’s a similar command called bundle exec rake test_app
which does basically the same thing.
With the dummy application inside the engine, the engine is mounted onto that application and the tests are then run against that application. No need to switch back and forth from application to engine.
If you have modifications for the engine inside your application, there’s also a way to test that. With Spree, we provide a module called
Spree::Core::UrlHelpers
that you can include into your RSpec.configure
block like this:
require 'spree/core/url_helpers'
RSpec.configure do |c|
c.include Spree::Core::UrlHelpers, :type => :integration
end
Then in your integration tests it’s simply a matter of referencing the Spree routes like this:
visit spree.products_path
Now for when you want to test that a customization to a Spree controller is behaving as intended, we’ve got a module for that too. It’s called
Spree::Core::TestingSupport::ControllerRequests
and you can include it like this:
require 'spree/core/testing_support/controller_requests'
RSpec.configure do |c|
c.include Spree::Core::TestingSupport::ControllerRequests, :type => :controller
end
Then you can write your controller specs like this:
describe Spree::ProductsController do
it "can see all the products" do
spree_get :index
end
end
This will then allow you to make requests to the Spree engine from inside your application’s tests.
Conclusion
There’s absolutely no reason to mount engine routes directly on Rails.application.routes
, nor is there a requirement to switch back and forth. Integration
testing an engine is extremely easy once you know how to do it.