Upgrading major versions of Rails sucks. I was there from 0.9 to 1.0, all the way to the famed 2 to 3 release, and they were all painful. I have good news though: Rails 4 is right around the corner, and it’s a much cleaner upgrade. While you are getting your app ready for Rails 4 are you bringing the gems you’ve written up to speed? If the answer is “I don’t know”, keep reading - we will cover how to test the gems you’ve written against multiple versions of Rails including Rails 4 release candidate.

Multiple Versions Matter

Believe it or not, there are still a large number of people who are using Rails 2 in production today. While the path to upgrading to Rails 3 is well documented (there’s a whole book on it) often the biggest blocker isn’t the app, or even the Rails frameworks. The largest hurdle to upgrading is often an un-maintained third party library that can’t be replaced or re-written and doesn’t work in the latest versions. There are many reasons to quit maintaining a library, but “I didn’t know how to upgrade it to support the latest rails release” shouldn’t be one of them.

Testing your App

Step zero is writing tests for your gem if you don’t already have them, once you’re done with that: make sure you’ve got a CI server running, I prefer Travis CI which is free for open source repos. Once you’ve got your CI build green, you’ll need to re-work your Gemfile so you can install multiple versions of Rails and then set up Travis to run your tests multiple times, each with a different version of Rails.

Make Ready your Gemfile

If you you haven’t already, delete your Gemfile.lock and remove it from git:

$ git rm Gemfile.lock

Then ignore the file by adding it to your .gitignore. On a stable app - Gemfile.lock stores the exact version of libraries that are used such as Rails 3.2.13. This is good for normal use, but we need our gem to be able to be bundle install-ed against any version of Rails. Removing the Gemfile.lock allows us to do that.

The next thing we will need to do is to enable your app to load multiple Rails versions from the environment open up your libraries’ Gemfile remove where you’re specifying Rails or railties version:

gem 'rails' '>= 3.1.0'

If it’s in the gemspec, you don’t need to do anything, now add this code

rails_version = ENV["RAILS_VERSION"] || "default"

rails = case rails_version
when "master"
  {github: "rails/rails"}
when "default"
  ">= 3.1.0"
else
  "~> #{rails_version}"
end

gem "rails", rails

Make sure to replace the numeric value in the "default" case with whatever you had specified previously.

Now you can bundle different versions of Rails like so

$ RAILS_VERSION=3.1.0 bundle update
$ RAILS_VERSION=3.1.0 bundle exec rake test

or if you want a one liner:

$ export RAILS_VERSION=3.1.0; bundle update; bundle exec rake test

This will install Rails version ~> 3.1.0 and run tests against it. To run tests against Rails master you can use RAILS_VERSION=master and to run against the betas or release candidates you can run RAILS_VERSION=4.0.0.pre

Now you can run tests on your app using different versions of Rails. You can see an example of this style of Gemfile in my projects oPRO and Wicked. The next step is to configure Travis to automatically do this for you.

Running Multiple Rails Versions on Travis

In your project create a .travis.yml file if you haven’t already. You can add environment variables to your travis run matrix by adding them to this file:

env:
  - "RAILS_VERSION=3.1.0"
  - "RAILS_VERSION=3.2.0"
  - "RAILS_VERSION=4.0.0.pre"
  - "RAILS_VERSION=master"

This will make travis run your project 4x more times each with a different environment specified. While we’re testing against different versions of Rails, let’s make sure we’re testing against different versions of Rubies too. This will test MRI 1.9.3, 2.0.0, and master as well as JRuby in 1.9 mode:

rvm:
  - 1.9.3
  - 2.0.0
  - ruby-head
  - jruby-19mode

Any time you’re testing the head or master of a project such as ruby-head you may see failres because the branch is unstable and not due to your project. For these cases you may want to allow failures until official versions are released.

matrix:
  allow_failures:
    - env: "RAILS_VERSION=master"
    - rvm: ruby-head

This will still run tests against the most recent version of Ruby and against Rails master, but if they fail Travis will not flag the build as failed. If you want to fine tune your allowed failures you can add them together like this:

matrix:
  allow_failures:
    - env: "RAILS_VERSION=3.0.0"
      rvm: 2.0.0

This will allow failures against Rails version 3.0.0 only when using Ruby 2.0.0. You can see the .travis.yml files for oPRO and wicked for full examples.

Test with Pull Requests

Once you’ve got your .travis.yml and Gemfile set up. Commit your results to a branch and push to github:

$ git checkout -b test-rails-4
$ git add .
$ git commit -m "testing rails 4"
$ git push origin test-rails-4

Now go to your repo on Github and open up a pull request like this PR on wicked. If you have PR testing turned on with Travis, this will kick off a build. If you’re lucky all of your required tests will pass and it will look like this:

You can see the Travis build for Wicked here.

If your tests don’t all pass, not to worry: look at the output, reproduce locally, fix, push, and repeat. Sometimes you might not be able to get one version of your library to support all the versions of Rails/Ruby that you need, when that happens you’ve got diverging stable versions.

Diverging Stable Versions

Rails no longer supports version 2.0, but maintains version 3.2+ and 4.0.0.RC1+. Most of your users won’t immediately upgrade to the latest version, and while you’re waiting for them to do so, you may find security or other bugs in a version of your library. For this reason I encourage you to consider supporting at least two versions of Rails. There are a few different strategies you can take in order to accomplish this goal:

Do nothing: If you’re lucky your gem will work unmodified in all versions of Rails, and you’re good to go. Make sure you’re tested, and don’t introduce backwards incompatable changes later down the road.

Branch: Rails uses versions and branches to manage it’s codebase and you can too. If your gem needs a major overhaul to be compliant with the latest release you may want to branch out your codebase. One branch for Rails 3 and you can likely leave Rails 4 on master.

$ git checkout -b rails3

You’ll want to have clearly defined versions for your which version of rails your gem supports. You may want to break semver for a single gem push and shadow Rails versioning. So the Rails3 compatible code can be found in version 3.X and the Rails4 compatible code can be found in 4.X of your gem. Once you do this, release as normal using semver, just avoid rev-ing the major version if you don’t have to.

Separate Gems: If keeping different copies of code in different branches seems too hard, break out your gems into multiple libraries. If your library is named foo consider forking it and making a foo_rails4 gem. While easier on you, it makes it harder for your users to upgrade since they’ll have to know you released a separate gem.

Testing Other Software Versions

You may find that your library depends on other libraries for development or production that have their own dependencies on Rails. One popular library is Devise. You can see how we conditionally change the version of Devise based on our Rails version in oPRO’s Gemfile, I got this little trick from Steve’s work on Draper’s Gemfile.

JRuby Note

While upgrading to Rails 4 I found that a JRuby Gem doesn’t play nice. If you’re testing JRuby and using the JRuby Sqlite3 adapter for ActiveRecord you may need to specify this in your gemfile:

  gem "activerecord-jdbcsqlite3-adapter", '>= 1.3.0.beta', :platform => :jruby

If you’re not testing JRuby in your Travis matrix, why aren’t you? You can always set it as an allowed failure and then at least people who want to know if your lib works with JRuby can check the Travis builds.

The Importance of Release Candidates

Now that you know how to write tests for your libraries to run on multiple versions of Rails, you have no excuse for not having them fully tested and compatible when Rails4 is fully released. It’s important to test against the Release Candidates (RC’s) because these are what will eventually become the fully released version of Rails. If you wait till Rails4 is released and find a bug in the framework, it may be too late to fix. When I was testing Wicked, I found and fixed a regression from 3.2.13 to 4.0.0.RC1. Even better, you’ll know if your library works with Rails4 or not, and so will your users.

If are a connoisseur of Hacker News, you may remember a set of inflammatory comments towards the Rails release team over a number of regressions introduced in a minor version bump of the library. The thing is: there was a release candidate for that minor version and for all of the people who wailed and gnashed their teeth at the regressions, few if any bothered to test against the release candidate. Next time your app gets caught by a regression, ask why you didn’t catch it before that version was released. You could be part of the solution.

Test Today

With bundler and Travis CI testing against multiple versions of Rails couldn’t be easier. Don’t be the maintainer of that one Gem that isn’t compatible with the Rails4 release. Test your gems against all the Rails versions today.

update: @shime_rb pointed out there is a gem called Appraisal to help if you don’t want to manage your Gemfile manually. You can see an example travis.yml with appraisal. Travis also supports specifying multiple gemfiles which is how devise tests against multiple versions of rails .


Richard Schneeman works for Heroku and is married to Ruby, literally. He wrote the easiest way to get started helping in open source: Code Triage. Follow him on twitter @schneems.