Blog

Start your engines!

By Ryan Bigg, 20 Oct 2011

Engines are a new feature within Rails 3 which allows for code such as that found in models and controllers to be shared in gems across applications. You may be familiar with the Devise gem, which is a very advanced engine. In this guide we cover how the foundation for the forem engine was laid.

A lot of people have been saying there's no real documentation or examples for Rails engines at the moment and that somebody should do something about it. Coincidentally, Rails 3 in Action will (eventually) have a chapter on creating an engine which will go into more detail than this guide, but this guide should help you get set up with an engine, as well as showing one setup of testing which uses RSpec + Capybara.

Hopefully this post can give you a nice kickstart on developing your own engine and you can learn from what we've gone through.

Generating an engine

To begin with, I'm going to assume that you've got RVM installed and know how to use it. We're going to be using it in this guide to separate any install we've got of Rails from the edge install that we'll be doing. I'll recommend here to use 1.9.2, but it's not absolutely required for this guide. It's just the latest + coolest.

Switch into a brand new gemset by running this command, which will also create an .rvmrc file in our engine:

    rvm use 1.9.2@rails-edge --create --rvmrc

Our first step will be installing Rails edge which contains an engine generator mainly by Piotr Sarnacki who has a guide of how to get started with engines too. We're going to take a slightly different approach here.

We can get edge rails by running this command:

    git clone git://github.com/rails/rails

And then to install it we need to cd into the directory and run rake install:

    cd rails
    rake install

When we do gem list we should see that we've now got rails (3.1.0.beta) along with the other Rails gems. We can also double check this by running rails -v which will show us that we've got "Rails 3.1.0.beta" too.

Now with Rails installed, we can use it to generate the scaffold of our engine. We're going to call it "forem":

    rails plugin new forem --mountable

The scaffold of this engine comes with the following helpful things:

  • forem.gemspec: Each plugin that is generated using the new Rails plugin generator now comes with a gemspec which allows it to be used as a gem. Like in the gem development guide, this is where we need to specify the dependencies of our engine / gem so that when people install it the proper dependencies are installed.
  • Gemfile: This file contains the rails and sqlite3-ruby gems as dependencies, but these should go into the gemspec. Because we're developing on an edge version of Rails, we may wish to change this line to point to the Git repository instead.
  • app: Like a standard Rails application, an engine contains an app folder. Inside this folder we've got the standard application controller, helper and layout, except these are all nested under the forem namespace so that they're only available to our engine.
  • config/routes.rb: Again, like a standard Rails application, an engine has a config/routes.rb file. This is where we define the routes for our engine. We can then mount our engine using mount Forem::Engine, :at => "forum" in our application to host our engine at that path.
  • lib/forem.rb: Defines the module Forem which is where any helper code can go, such as configuration settings.
  • lib/forem/engine.rb: Defines the Forem::Engine class, which is the main "contact point" of applications and this engine.
  • public/stylesheets and public/javascripts: Engines in Rails 3.1 can now have their own static assets, which is what these two directories provide. Note that this has given us the prototype files, but we can get it generate jQuery ones by passing the -j jquery option to the generator.
  • script/rails: Allows us to use the familiar rails server, rails generate and rails console commands for our engine. Exceptionally handy.
  • test: A relic from years gone by, we'll replace this directory with a spec one shortly.
  • test/dummy: The most fun part of an engine. This is actually a mini-Rails app in your engine. When we run the tests later on, the suite will boot this application which includes our engine and then perform requests on it. We'll move this directory to spec/dummy after we install RSpec.

One more interesting thing here is that, because we passed --mountable to our generator, in test/dummy/config/routes.rb there's this line:

    mount Forem::Engine, :at => "/forem"

This is the line used to connect the engine's routes to the application's, and will prefix all of the engine's routes with "/forem". It's not compulsory that this be placed at the "/forem" path, it could be anything. Any routes prefixed with this path will now be served by the engine rather than the application.

When developing this engine, we're probably going to want to make sure that it's working as we want it to. We could use the test directory that comes with engines, but this is not 2001. We're going to use RSpec instead. (Oh, and RSpec's the preferred testing framework in the Rails community)

Installing RSpec

First, we need to inform our engine of a new dependency for development and we can do this by opening up the forem.gemspec file and just before the end of the Gem::Specification.new block we can put this line:

    s.add_development_dependency "rspec-rails", "~> 2.5"

The Gemfile has the gemspec method called in it, which means that we can run bundle install and Bundler will read the dependencies of our gemspec as well as those specified in the Gemfile. Let's run this command now to install this version of RSpec.

When we run rails g rspec:install in a Rails application, it creates a spec/spec_helper.rb file for us. If we ran it inside an engine, it would create the same spec/spec_helper.rb file as what's needed in an application. This isn't quite what we need. Our spec/spec_helper.rb needs to load the dummy application's config/environment.rb file rather than one that is supposed to be a couple of levels up, for starters.

The first thing we'll do here is move the test/dummy application into a new folder of spec/dummy and remove the test directory altogether. After this we'll need to update the location in the Rakefile as well as script/rails to now point to spec/dummy instead of test/dummy.

Next, we'll create the spec/spec_helper.rb file and fill it with this content:

    # Configure Rails Envinronment
    ENV["RAILS_ENV"] = "test"
    require File.expand_path("../dummy/config/environment.rb",  __FILE__)

    require 'rspec/rails'

    ENGINE_RAILS_ROOT=File.join(File.dirname(__FILE__), '../')

    # Requires supporting ruby files with custom matchers and macros, etc,
    # in spec/support/ and its subdirectories.
    Dir[File.join(ENGINE_RAILS_ROOT, "spec/support/**/*.rb")].each {|f| require f }

    RSpec.configure do |config|
      config.use_transactional_fixtures = true
    end

At the top of this file we set the Rails environment to test and then require the dummy application's config/environment.rb file. This initializes the Dummy application so that we can then perform tests on it as we please.

Next, we set an ENGINE_RAILS_ROOT variable that points at the root of our engine. This is done so that we can access it in our tests or RSpec configuration, which we do on the next line of code. The line of code beginning with Dir[ requires all the files in the spec/support directory and all it's sub folders. These files should set up extra configuration for RSpec testing (such as routes.rb which includes the engines routes, oops spoilers). It's just a good way of separating out configuration into easy-to-manage chunks.

With our spec directory set up we can go ahead and create spec/controllers and spec/models directory and put our tests in there just like we would an application. Remember though, these files should be under the namespace of the engine so that they don't conflict with any files in the main application!

While controller and model tests are fun, full-on integration tests is a better place to start. We can do this using RSpec + Capybara. It can also be done using Cucumber, but I won't cover it in this post.

Installing & Using Capybara

Capybara's a browser simulator which we can use for testing our engine. It supports a number of drivers such as Rack::Test and Selenium and has a kick-ass API.

To get started with Capybara we're going to need to add this as another development dependency to our forem.gemspec, which we can do just under the rspec line like this:

    s.add_development_dependency "capybara", "~> 0.4"

When we run bundle install again, this time it'll install the latest 0.4 version of Capybara (as of this writing, that's 0.4.1.2).

In this situation, we can use Capybara for integration testing that goes a little like this:

    require 'spec_helper'

    describe "forums" do
      before do
        @forum = Forem::Forum.create!(:title => "Welcome to Forem!",
                                     :description => "A placeholder forum.")
      end

      it "listing all" do
        visit forums_path
        page.should have_content("Welcome to Forem!")
        page.should have_content("A placeholder forum.")

      end

      it "visiting one" do
        visit forum_path(@forum.id)
        within("#forum h2") do
          page.should have_content("Welcome to Forem!")
        end
      end
    end

Some people may prefer this syntax as all the code is there in Ruby, in the test. Personally, I can do whatever people on my team do.

This code goes in the spec/integration/forums_spec.rb file and uses a couple of methods, such as visit and within from Capybara. So to get this to work we need to include Capybara within our RSpec configuration, which we can do in spec/spec_helper's RSpec.configure block, changing it to this:

    RSpec.configure do |config|
      config.use_transactional_fixtures = true
      config.include Capybara, :example_group => { :file_path => /\bspec\/integration\// }
    end

By including Capybara like this, we make the methods from this module available in all specs in the spec/integration directory, but no other.

In the above example we make reference to the forums_path and forum_path routing helpers, which aren't yet defined. Routes for an engine are defined in config/routes.rb, just like in a normal Rails application. We can define the routing helpers for our test by making our engine's config/routes.rb this:

    Forem::Engine.routes.draw do
      resources :forums
    end

Now, these routing helpers aren't available immediately even still. At the moment, because of how our application is loaded, we've only got access to the routing helpers from the dummy application. To gain access to the engine's routes we must add this line to RSpec.configure in spec/spec_helper.rb:

    config.include Forem::Engine.routes.url_helpers

Now we'll be able to run our test with our routes, and so we've got a great place to begin developing our application using RSpec + Capybara.

Release the hounds engine

To release this engine, the best way would be to make it into a gem. Luckily, we've already got the needed gemspec file for this, but we're lacking the Rake task to put it on Rubygems. We can add these Rake tasks to our project by adding this line to our Rakefile:

    Bundler::GemHelper.install_tasks

This will give us the necessary rake build and rake release commands we need to build and release the engine out to the world.

One more thing we need to configure is the files that will be included with our engine when it is packaged as a gem. Currently in forem.gemspec there's this line:

    s.files = Dir["lib/**/*"] + ["MIT-LICENSE", "Rakefile", "README.rdoc"]

This will only include files from the lib directory and the MIT-LICENSE, Rakefile and README.rdoc files. We'd like it to contain everything, and so we will replace that line with this one:

  s.files = `git ls-files`.split("\n")

And now we'll be able to push this gem out to the world and people will be able to use it within their applications.

As an example of an engine that does this already, check out the forem engine project, which aims to be a basic forum system (one day!) and was the basis for this post.

blog comments powered by Disqus