Ryan Bigg

Integration testing engines

26 Apr 2012

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.

blog comments powered by Disqus