labs

Object Oriented Rails – Writing better controllers

I have been doing a lot of mobile development lately, using Objective-C on iOS, and Java on Android. Since I’m also a Ruby developer, it makes a lot of sense for me to try and apply what I learn from each of these languages and frameworks whenever I can.

Today I’m going to do my best in writing better controllers in my Rails app. When writing an app for Android, or iOS, I force myself not to use stubs or mocks, but instead I try to improve the object architecture and use dependency injection to write my testing code.

Let’s take an example of code:

class User
  validates_uniqueness_of :username
end
class RegistrationController < ApplicationController
  def create
    user = User.where(username: params[:username]).first
    user ||= User.new.tap do |new_user|
      new_user.username = params[:username]
      new_user.save!
    end
    render json: user
  end
end

Here we only need a username to register the user, and if the user is already registered we just return it. Very simple. But that logic does not belong to a controller, and it would be impossible to test it without connecting to that database and without using a pretty big mess of stubs and mocks. A good indicator of “you’re doing it wrong” would be having: User.any_instance in your specs.

A first improvement could be the following:

class User
  validates_uniqueness_of :username

  def self.register(username)
    user = User.where(username: username).first
    user ||= User.new.tap do |new_user|
      new_user.username = username
      new_user.save!
    end
  end
end
class RegistrationController < ApplicationController
  def create
    user = User.register(params[:username])
    render json: user
  end
end

This is already much better code, you only need to stub User.register to be able to test that controller. And that is just fine.

Now you could just stop there, and if your app does not grow too much you can handle it pretty nicely. But if your app starts growing, your User model will grow bigger and bigger, you will start giving it more responsibility, more than just knowing: “Am I valid?”.

A better way of splitting that and actually getting rid of stubs would be that final piece of code:

class User
  validate_uniqueness_of :username
end
class RegistrationService
  def register(username)
    user = User.where(username: username).first
    user ||= User.new.tap do |new_user|
      new_user.username = username
      new_user.save!
    end
  end
end
class RegistrationController < ApplicationController
  before_filter :load_registration_service

  def create
    user = @registration_service.register(params[:username])
    render json: user
  end

  def load_registration_service(service = RegistrationService.new)
    @registration_service ||= service
  end
end

The registration service will only be responsible for registration related database access. Note that this service could be switched to another one that actually would use an Http service instead of the database. Also now you can entirely get rid of stubs in your tests, as follows:

describe RegistrationController do
  before do
    @fake_registration_service = controller.
        load_registration_service(FakeRegistrationService.new)
  end

  describe "POST #create" do
    it "delegates to the registration service and renders the returned user" do
      expected_user = User.new.tap {|u| u.username = "damien"}
      @fake_registration_service.registered_user = expected_user

      post :create, username: "damien"

      expect(response.status).to eq(200)
      expect(response.body).to eq(expected_user.to_json)
      expect(@fake_registration_service.username_registered).to eq("damien")
    end
  end
end
class FakeRegistrationService
  attr_accessor :registered_user
  attr_reader :username_registered

  def register(username)
    @username_registered = username
    registered_user
  end
end

Sadly, we don’t control in Rails how our controllers get instantiated. So we need to use something like that trick with the before filter using a default value for our service. With a PORO you would define a constructor that does something similar.

Dependency injection is very common when test driving Android apps and iOS apps. I personally like to never use mocks on these platforms. And I’m trying to do more of it in Ruby on Rails applications. It will force you to think your object architecture better, having more dedicated objects, limiting logic in value objects (like models and presenters)…

Let me know what you think of this! 🙂