How to test your gem against multiple version of Rails?

19 Feb 2014 By: Greg Molnar

If you develop a gem which integrates with Rails you may want to test against multiple versions of it. My solution to this to use one page Rails applications with the appraisals gem and load the applications based on the actual gem version. Let me show you an example:

bundle gem example
      create  example/Gemfile
      create  example/Rakefile
      create  example/LICENSE.txt
      create  example/README.md
      create  example/.gitignore
      create  example/example.gemspec
      create  example/lib/example.rb
      create  example/lib/example/version.rb
Initializing git repo in ~/Github/example

We generated a gem skeleton. Let's prepare the gem for testing. We need to add some dependencies to the gemspec:

# example.gemspec
...
spec.add_development_dependency 'minitest', '>= 3'
spec.add_development_dependency 'appraisal'
...

Create the test folder with the test helper:

# test/test_helper.rb
require 'bundler/setup'
require 'minitest/autorun'
require 'example'

ENV["RAILS_ENV"] = "test"
ENV['DATABASE_URL'] = 'sqlite3://localhost/:memory:'

require "apps/rails4"

Setup rake to run the tests:

require "bundler/gem_tasks"
require 'rake/testtask'
require 'appraisal'

Rake::TestTask.new do |t|
  t.libs = ["test"]
  t.pattern = "test/**/*_test.rb"

end
task :default => :test

Create an Appraisal file:

# Appraisals
appraise "rails-4" do
  gem "rails", "4.0.0"
  gem "sqlite3"
end

Than run bundle install and rake appraisal:install to generate the gemfile. The official Rails guide advises to use a dummy Rails app for testing but in their guide it is a structured setup and I think it is better to keep everything in one file in this scenario so we will create a one file Rails application:

# test/apps/rails4.rb
require "rails"
require 'rails/all'
require 'action_view/testing/resolvers'
require 'rails/test_help'

require 'example' # our gem

module Rails4
  class Application < Rails::Application
    config.root = File.expand_path("../../..", __FILE__)
    config.cache_classes = true

    config.eager_load = false
    config.serve_static_assets  = true
    config.static_cache_control = "public, max-age=3600"

    config.consider_all_requests_local       = true
    config.action_controller.perform_caching = false

    config.action_dispatch.show_exceptions = false

    config.action_controller.allow_forgery_protection = false

    config.active_support.deprecation = :stderr

    config.middleware.delete "Rack::Lock"
    config.middleware.delete "ActionDispatch::Flash"
    config.middleware.delete "ActionDispatch::BestStandardsSupport"
    config.secret_key_base = '49837489qkuweoiuoqwehisuakshdjksadhaisdy78o34y138974xyqp9rmye8yrpiokeuioqwzyoiuxftoyqiuxrhm3iou1hrzmjk'
    routes.append do
      get "/" => "welcome#index"
    end
  end
end

class WelcomeController < ActionController::Base
  include Rails.application.routes.url_helpers
  layout 'application'
  self.view_paths = [ActionView::FixtureResolver.new(
    "layouts/application.html.erb" => '<%= yield %>',
    "welcome/index.html.erb"=> 'Hello from index.html.erb',
  )]

  def index
  end

end

Rails4::Application.initialize!

Than write an integration test which uses this controller:

# test/integration/welcome_controller_test.rb
require 'test_helper'

class WelcomeControllerTest < ActionController::TestCase

  test "should get index and our gem should do it's job" do
    get :index
    assert_response :success
    # do your gem specific assertion
  end
end

If we run rake appraisal now it will run this test and it will pass. Next step is to create a Rails 3 test app. First we need to setup the appraisal for that:

# Appraisals
...
appraise "rails-3_2" do
  gem "rails", "3.2.14"
  gem "sqlite3"
  gem "test-unit"
end

Run rake appraisal:install. We need to change our test helper to switch beetween the Rails apps based on the Rails version:

# test/test_helper.rb
require 'bundler/setup'
require 'minitest/autorun'
require 'example'

ENV["RAILS_ENV"] = "test"
ENV['DATABASE_URL'] = 'sqlite3://localhost/:memory:'
require "rails"
case Rails.version
when '3.2.14'
  require 'test/unit'
  require "apps/rails3_2"
when '4.0.0'
  require "apps/rails4"
end

Than we need to create the test application:

# test/apps/rails3_2.rb
require 'rails/all'
require 'action_view/testing/resolvers'
require 'rails/test_help'

require 'example' # our gem

class Rails3_2 < Rails::Application
  config.root = File.expand_path("../../..", __FILE__)
  config.cache_classes = true

  config.eager_load = false
  config.serve_static_assets  = true
  config.static_cache_control = "public, max-age=3600"

  config.consider_all_requests_local       = true
  config.action_controller.perform_caching = false

  config.action_dispatch.show_exceptions = false

  config.action_controller.allow_forgery_protection = false

  config.active_support.deprecation = :stderr

  config.middleware.delete "Rack::Lock"
  config.middleware.delete "ActionDispatch::Flash"
  config.middleware.delete "ActionDispatch::BestStandardsSupport"
  config.secret_token = "49837489qkuweoiuoqwehisuakshdjksadhaisdy78o34y138974xyqp9rmye8yrpiokeuioqwzyoiuxftoyqiuxrhm3iou1hrzmjk"
  routes.append do
    get "/" => "welcome#index"
  end
end

class WelcomeController < ActionController::Base
  include Rails.application.routes.url_helpers
  layout 'application'
  self.view_paths = [ActionView::FixtureResolver.new(
    "layouts/application.html.erb" => '<%= yield %>',
    "welcome/index.html.erb"=> 'Hello from index.html.erb',
  )]

  def index
  end

end

Rails3_2.initialize!

Now if you ru rake appraisal your test suite will be run against both versions of Rails'.

The repo for the source of this example: multiple-rails-test-example

PS: If you want to get updates from me please subscribe to my email list.
I hate spam as much as you do, so I won't send you anything else than Ruby/Rails related updates occasionally, and of course you can unsubcribe anytime.