Upgrading a Ruby on Rails application

By Mario Alberto Chavez February 14, 2017

rails ruby upgrade minitest security vulnerabilities performance

Not every Ruby on Rails application that we work on at michelada.io is a Rails application running in the latest version, I’ll say that probably 70% are still running on Ruby on Rails 4.2, 25% are running on Rails 5 and the rest on Rails 3.2.

Rails 5 numbers are for applications that we have started for our clients since the Rails team released the first beta of version 5 in 2015.

For Ruby 4.2 applications, are applications that we started for our clients before Rails 5 beta or applications that our clients started on their own and we have taken ownership or we are collaborating with its engineering team to support and adding new features.

Both versions as today are still being supported by the Rails team with security patches and bug fixes. Pretty much the same apply with most of the gems that we use in those projects.

Rails 3.2 applications that some our clients still keep around are applications in maintenance mode, means no new big features are added but small tweaks might get into their codebase time to time.

Applications running in this version are subject to be rewritten from scratch with a newer version of Rails. From our experience with our clients, this happens when the core logic of the application is what the client wants but everything else is not that important, like converting the application to be an API only with the base logic.

The other case is when a client asks to perform an upgrade from 3.2 to 5. This just happened in my last/current project and I want to talk in this post.

Why upgrade your Rails application?

Reasons on why you might want to upgrade your application may vary case by case. It might not be driven because of business needs but primary due to technical needs or concerns.

Security and bug fixes

Security is a critical reason to decide to perform an upgrade, this is not only a technical issue but business also; no one wants to deal with a problem where your user’s information might be compromised.

Rails and Ruby have a very similar maintenance policy where only the latest release of a series will be maintained.

For Rails, currently, this means 4.2 and 5.0. Version 4.1 was maintained by a special effort of Rafael França but once that Rails 5.1 is released - possible this 2017 - there is no word if 4.2 will be maintained in the same way that 4.1 was.

Ruby 2.4 was released last December and along with Ruby 2.3 are still under maintenance policy, as today there is no word on End of Life of Ruby 2.2, on Ruby’s repository looks like bug fixes are still being backported to this version.

Even if you are at the latest version of your stack, you still need to ensure that your infrastructure and your code are secure. There are mailing lists for Ruby and Ruby on Rails where you can find out and follow Common Vulnerabilities and Exposures or CVE.

In your project you must have a policy on how often update versions of your dependency Gems, vulnerabilities exposures are also discovered on them. You can check every gem manually but if you want an automated solution to help you, use bundler-audit, it will work with your Gemfile to report any CVE in your dependencies.

Your own code might be also subject to vulnerabilities, Brakeman, which is a security scanner, might help you to discover insecure patterns in your code, also Gavin Miller’s blog is a great resource to learn how to write more secure code.

Being in the latest version in your stack will also help your team to avoid to have workaround code fix a bug, maybe, already fixed in latest versions. Doing workarounds makes harder to understand why that code exists, it might also introduce new bugs.

Performance

While always is nice to have our applications running as fast as possible, maybe not receiving this kind of updates will not degrade your performance but you will be missing the opportunity of a faster application without a change in your code, just by being the latest version of your stack.

Many improvements have been introduced to Rails and Ruby in their latest versions to reduce object allocations, code optimization and in general to have Ruby applications to perform better.

Ok, you said that this post was about how to upgrade a rails 3.2 application to Rails 5.0

Now going back to the main subject of this post a client handed me a Rails 3.2 project, they wanted to add new functionality, convert the application to be an API only and upgrade it to the latest version possible.

I will not go into a step by step migration process because it might be different for each project but here is a list of things that you need to watch out to have a successful migration.

Test suite

When you are new to a Rails application codebase, the first place to check is always project’s automated tests. In the case of this application, it had RSpec 2.x specs but 50% of the specs were failing, not a good start point.

Given that required new functionality was a priority I decided to go ahead and implement it, the only thing that I did before startiing the process, was to upgrade RSpec from version 2.x to version 3.x. Now besides failing specs, I had a bunch of warnings because of the should syntax of RSpec 2.x.

I implemented the new functionality while adding new specs and fixing the ones failing that were related to what I was working on, at the same time I was updating RSpec syntax.

Also, project specs were using machinist as test data factory. Machinist is no longer maintained and the data model wasn’t really complicated, so I move the project to use Rails Fixtures.

After new functionality was in place, my next step was to fix remaining failing specs while moving them away from machinist and updating RSpec syntax, also, I converted controller specs to be integration specs.

Few days laters our test suite was green, and I was ready to start with Rails migration.

Talking with a co-worker about RSpec syntax migration he told me about transpec which is a gem to help to automate syntax migration, my work here was already done, so I don’t have the chance to try it out.

Rails migration

With a green test suite, I was ready to start Rails migration. Just for the sake of the test suite, I relaxed versions in Gemfile for all gems and the run a bundle update, next I ran the test suite and it was still green.

The first step was to modify the Gemfile to add Ruby version, in my case, it was 2.3.1, and also to change Rails version to be 5.0.1. Additionally I changed sass-rails version to be ~> 5.0, coffee-rails to ~> 4.1 and uglifier to >= 1.3.0, also I added puma as dependency. At this point, conversion to be an API only is not done, that is why these gems are still a dependency.

Then I ran bundle update and it finished without an issue. This might be different depending on the gems that your projects rely on. If specified gem is not compatible with Rails 5 bundler might fails. Probably if I had keep machinist gem this one might cause bundler to fail.

Configuration settings are something that changes all the time between different versions of Rails. To upgrade configuration settings you need to run rake app:update, this will create new files like cable.yml but also it will try to replace existing files like application.rb, environment.rb, production.rb and so on, but before overwriting the files it will ask you what do you want to do.

You can skip the file, overwrite it, overwrite all, see a diff or about the operation. On all files I displayed the diff to see changes on file, then I proceed to overwrite the file, open it with an editor and put back what I got from the diff that I wanted to keep like config.autoload_paths += %W(#{config.root}/lib) in application.rb. There were files like boot.rb or en.yml that I just replaced because there was nothing to keep there but also there were files like routes.rb that I skipped from being overwritten.

After this process was done, I tried to load Rails console, and it loaded with success, but we were not ready to launch the server yet. I ran the test suite and pretty much all specs failed also I did receive many warnings.

So far all failures and warnings are related to changes in the API between Rails 3.2 and Rails 5. So let’s see what needs to be updated.

Our routes.rb file have many routes defined with match but no via parameter was specified, so here we have 2 options: add via parameter or convert match route to a http method, like:

match “/share/:id” => “shares#create”
# become:
match “/share/:id” => “shares#create”, via: [:post]
# or
post “/share/:id” => “shares#create”

Another required change is the use of strong parameters instead of the old and insecure attr_accessible method. Both methods work, mainly with controllers, to define what attributes in a model are subject to mass assignment.

For example, you can have a model Customer and you want to update its :name and :address from a web form, the way this worked before strong parameters was:

class Customer < ActiveRecord::Base
  attr_accessible :name, :address
end

class CustomersController < ApplicationController
...
  def update
    @customer = Customer.find(params[:id]
    if @customer.update_attributes(params[:customer])
      return redirect_to customers_path
    end
   end
 ...
 end

attr_accesible is not available anymore starting Rails 4.0 because it did prove to be an insecure way to protect your model’s data. The code presented before needs to be updated to:

class Customer < ActiveRecord::Base
end

class CustomersController < ApplicationController
...
  def update
    @customer = Customer.find(params[:id]
    if @customer.update_attributes(edit_params)
      return redirect_to customers_path
    end
  end
...
  private

  def edit_params
    require(:customer).permit(:name, :address)
  end 
end

This change needs to be done in all your models and controllers that create or update new records using Rails models.

ActiveRecord query API also changed and finders functionality was removed. The following queries are example of what was removed from Rails 5:

Customer.find(:all, conditions: { active: true }, limit: 10)
Customer.find_by_name("Customer 1")
Customer.find_last_by_active(false)
Customer.find_or_initialize_by_name("Customer 2")

Old hash syntax and dynamic finders need to be updated to use where clause.

Customer.where(active: true).limit(10)
Customer.where(name: "Customer 1")
Customer.where(active: false).last
Customer.find_or_initialize_by(name: "Customer 2")

For more details on the changes to the ActiveRecord query API look at deprecated finders.

After all these changes in the source code the test suite as green again, but still I got a lot of warnings but Rails was complaining because of 2 situations.

The use of before_filter and skip_before_filter in Rails controllers is deprecated and those methods will be removed in the future. Both were replaced by before_action and skip_before_action.

I did mention before when I was getting the Rails 3.2 ready to be migrated, I moved controllers specs to become integration specs. Within those specs, the controller logic is executed when a call like this happens:

post "/share/1", nil, { "X-Extra-Header" => "123" }

The first parameter is the Rails path to hit, next request parameters and finally a hash of headers to send. This API was updated to use keyword arguments, it means that instead of passing parameters by position, now we need to name parameters as follow:

post "/share/1", headers: { "X-Extra-Header" => "123" }

Given that we are not sending parameters it gets omitted in our call, but if you need to pass parameters to your call just call it with:

post "/share/1", headers: { "X-Extra-Header" => "123" }, params: { resource_id: 2 }

After these changes, my test suite was green and have no more warnings.

The application was deployed to an staging environment to Heroku for additional testing but it was failing when calling very specific end points. The error was something like NameError: uninitialized constant XXX where XXX was the name of the constant.

It turns out that the class file was present in Rails lib directory and it was loaded correctly in development and test environments thanks to the setting config.autoload_paths += %W(#{config.root}/lib) in our application.rb file, but it was not working in production.

Because autoloading is disabled in production with Rails 5, using autoload_paths will not load needed classes from specified paths. The solution to this, is to ask Rails to eager load classes, this can be done by replacing config.autoload_paths += %W(#{config.root}/lib) with config.eager_load_paths += %W(#{config.root}/lib).

After this final change, the migration was complete.

Additional bonus, replace RSpec with Minitest

Migration of the Rails project happened very smoothly, having the test suite covering important areas of the application helped a lot.

After finishing the upgrade and given that I’m a Minitest fan, I wanted to find out how much effort required to migrate from RSpec to Minitest. It was very simple to migrate, I was able to automate something between 90% and 95% the migration of tests and only a few of them required to review the test and manually migrate it.

First I renamed the spec directory to test, next in Gemfile I removed RSpec dependencies and added minitest-rails. After running bundle update I ran rails g minitest:install to generate a test_helper.rb file.

I copied from spec_helper.rb file what did make sense to have in the new test_helper.rb, this will depend on your project.

Next, I used command rename to rename all files from *_spec.rb to *_test.rb. rename can be installed in MacOS using Homebrew.

find . -name '*_spec.rb' -exec rename 's|_spec|_test|' {} \;

Now with the following commands, I automated most of the syntax changes between RSpec and Minitest.

find . -name '*_test.rb' -exec sed -ie 's/spec_helper/test_helper/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/context/describe/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/stub(/stubs(/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/double/stub/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/should_receive/expects/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/and_return/returns/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/expect(//g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/).to be ==/.must_equal/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/).to eq/.must_equal/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/).to be_empty/.must_be_empty/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/).to_not be_empty/.wont_be_empty/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/).to be_kind_of/.must_be_kind_of/g' {} \;
find . -name '*_test.rb' -exec sed -ie ’s/(response).to have_http_status/.must_respond_with/g’ {} \;
find . -name '*_test.rb' -exec sed -ie 's/).to redirect_to/.must_redirect_to/g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/RSpec.//g' {} \;
find . -name '*_test.rb' -exec sed -ie 's/).to match/.must_match/g' {} \;

After this was done, it was a matter of running the test suite and fix any failing test. All failures were related to changes in syntax and as I said before it was a very small number of tests, most of them just passed without a problem.

The only gotcha is that integration tests in Minitest with the spec syntax need to be named in a way that it ends with Integration Test, like Share a resource Integration Test.

This post was my initial reference for this migration Switching from RSpec to Minitest.

Conclusions

Migration of this project for our client wasn’t so bad, it took me about a week to complete it. The application wasn’t too big and I just did the minimal changes to make it work in Rails 5. It still has a lot of room for cleaning and refactoring but for now, our client is satisfied to be running with the latest stack of Ruby and Rails.

If your team is looking for help to migrate a Rails application from any older version to a newer one, please contact us at info@michelada.io we will be more than happy to help, no matter if is a small or a big project.


Mario Alberto Chavez

Software Engineer