Rails 3.1 Engines, Rspec, and Cucumber

I thought I’d make a quick post about my first encounter with Rails 3.1 and with engines in general.  Rails engines are essentially applications or a subset of functionality that can be run in or shared with other applications.  In Rails 3.1, creating an engine is as easy as:
rails plugin new engine_name [options]

where several options are available, including full and mountable. Run with
--help to find out more.

Generating a new engine produces a file structure similar to a regular rails application, but with a few notable differences which have been explained in several great articles.

Setting up Rspec

I enjoy using rspec in my testing and instead of using the test folder which was generated automatically, I decided to rename it to spec. Inside this spec folder, I set up the usual rspec environment. For example, inside my spec folder, I have additional folders named models, controllers, views, support, etc. Rspec needs to load the dummy application’s environment in order to function. To allow it to do so, a few changes are necessary to spec_helper.rb.

# /spec/spec_helper.rb

# This file is copied to spec/ when you
# run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../dummy/config/environment", __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[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
Dir[File.join(ENGINE_RAILS_ROOT, "spec/support/**/*.rb")].each {|f| require f }

RSpec.configure do |config|
  # == Mock Framework
  #
  # If you prefer to use mocha, flexmock or RR, uncomment
  # the appropriate line:
  #
  # config.mock_with :mocha
  # config.mock_with :flexmock
  # config.mock_with :rr
  config.mock_with :rspec

  # Remove this line if you're not using ActiveRecord
  # or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run
  # each of your examples within a transaction, remove
  # the following line or assign false instead of true.
  config.use_transactional_fixtures = true

  config.include Cornerstone::Engine.routes.url_helpers
end

Of course, renaming the test folder to spec did have some unintended consequences. For one, running rake tasks did not work. In order to fix that, I had to change Rakefile to load the dummy application’s environment from the new location.

# /Rakefile

#!/usr/bin/env rake
begin
  require 'bundler/setup'
rescue LoadError
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
end
begin
  require 'rdoc/task'
rescue LoadError
  require 'rdoc/rdoc'
  require 'rake/rdoctask'
  RDoc::Task = Rake::RDocTask
end

RDoc::Task.new(:rdoc) do |rdoc|
  rdoc.rdoc_dir = 'rdoc'
  rdoc.title    = 'EngineName'
  rdoc.options << '--line-numbers' << '--inline-source'
  rdoc.rdoc_files.include('README.rdoc')
  rdoc.rdoc_files.include('lib/**/*.rb')
end

# notice the path change in the following line
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
load 'rails/tasks/engine.rake'

require 'rake/testtask'

Rake::TestTask.new(:test) do |t|
  t.libs << 'lib'
  t.libs << 'test'
  t.pattern = 'test/**/*_test.rb'
  t.verbose = false
end

task :default => :test

Bundler::GemHelper.install_tasks

Finally, running rails on the console will not work until you change the dummy application’s environment reference in the rails script.

# /script/rails

#!/usr/bin/env ruby
#!/usr/bin/env ruby
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.

ENGINE_PATH = File.expand_path('../..',  __FILE__)
# notice the path change on the following line
load File.expand_path('../../spec/dummy/script/rails',  __FILE__)

I believe that’s all it took to get rspec working again after renaming the test folder to spec. Of course, since I’m using rspec, I no longer needed test_helper.rb and so I removed it. If there’s something I missed, or if you can’t get rspec to work, please let me know.

Preparing Cucumber

As with Rspec, Cucumber also needs to load the dummy application’s environment in order to function. After adding cucumber-rails to your gemspec or Gemfile, running rails generate cucumber:install will generate the necessary files and folder structure for cucumber to function properly. Unfortunately, the files generated are geared toward normal rails applications and not rails engines. To make it work for a rails engine, I had to make the following changes to env.rb.

# /features/support/env.rb

# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
# It is recommended to regenerate this file in the future when you upgrade to a
# newer version of cucumber-rails. Consider adding your own code to a new file
# instead of editing this one. Cucumber will automatically load all features/**/*.rb
# files.

ENV["RAILS_ENV"] = "test" # had to explicity state the test environment (or cucumber env.)
ENV["RAILS_ROOT"] = File.expand_path(File.dirname(__FILE__) + '/../../spec/dummy/')
require File.expand_path(File.dirname(__FILE__) + '/../../spec/dummy/config/environment')

require 'cucumber/rails'

# If you use factory girl, I had to add the following...
require 'factory_girl_rails'
require File.expand_path(File.dirname(__FILE__) + '/../../spec/support/factories')

# Capybara defaults to XPath selectors rather than Webrat's default of CSS3. In
# order to ease the transition to Capybara we set the default here. If you'd
# prefer to use XPath just remove this line and adjust any selectors in your
# steps to use the XPath syntax.
Capybara.default_selector = :css

# By default, any exception happening in your Rails application will bubble up
# to Cucumber so that your scenario will fail. This is a different from how
# your application behaves in the production environment, where an error page will
# be rendered instead.
#
# Sometimes we want to override this default behaviour and allow Rails to rescue
# exceptions and display an error page (just like when the app is running in production).
# Typical scenarios where you want to do this is when you test your error pages.
# There are two ways to allow Rails to rescue exceptions:
#
# 1) Tag your scenario (or feature) with @allow-rescue
#
# 2) Set the value below to true. Beware that doing this globally is not
# recommended as it will mask a lot of errors for you!
#
ActionController::Base.allow_rescue = false

# Remove/comment out the lines below if your app doesn't have a database.
# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead.
begin
  DatabaseCleaner.strategy = :transaction
rescue NameError
  raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
end

# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios.
# See the DatabaseCleaner documentation for details. Example:
#
#   Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do
#     DatabaseCleaner.strategy = :truncation, {:except => %w[widgets]}
#   end
#
#   Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do
#     DatabaseCleaner.strategy = :transaction
#   end
#

Note the two lines at the top which reference the dummy application. These lines are required because cucumber attempts to boot a rails application from the normal environment.rb which is usually located in the config folder. In a rails engine, this file doesn’t exist. It only exists within the dummy application. Adding the additional lines at the top of env.rb forces cucumber to bootstrap the dummy application’s environment. With these changes, cucumber should be fully functional.

One further change that had me stumped for a while was using Cucumber’s path_to helper located in paths.rb. I just could not make cucumber return the correct path when referencing using a path helper such as some_controller_method_path. In order to use these helpers (thanks Robin – see below) you can add the engines’ url_helpers to the World object of Cucumber like this:

module EngineRoutesHelper
  include MyEngine::Engine.routes_url_helper
end
World(EngineRoutesHelper)

Now, you will be able to rewrite the path_to method like this:

def path_to(page_name)
  case page_name
    when /^the discussions page$/
      discussions_path
    # etc…
  end
end

I hope my stumblings have helped others out there. Give me a shout if you run into any problems.

5 thoughts on “Rails 3.1 Engines, Rspec, and Cucumber

  1. Instead of using the engine_wrap helper method to access the isolated engine routes, you can add the engines’ url_helpers to the World object of Cucumber like this:

    module EngineRoutesHelper
    include MyEngine::Engine.routes_url_helper
    end
    World(EngineRoutesHelper)

    Now, you will be able to rewrite the path_to method like this:

    def path_to(page_name)
    case page_name
    when /^the discussions page$/
    :discussions_path
    # etc…
    end
    end

  2. I tried to use the rails plugin generator both with the –full and –mountable options. In neither case was a dummy app generated. Of course, you could think of the hosting app as the “dummy app” that you use to test that the engine works as expected, simply referencing the mountable app/engine using the gem :path option. I wonder if I should just use enginex to generate the dummy app within the engine in order to test the engine in isolation of the hosting app? Hmm…

    • Sorry for my late reply. I’ve been swamped lately and on top of all that I had to migrate this site to a new host.

      Which version of Rails did you try that with? I believe when I wrote the article, I was using at least 3.1. I just tried with 3.2.6 and I am getting the dummy app under /test/dummy for both a full and mountable engine.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>