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! 🙂