GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS

Written by thoughtbot

Inject that Rails Configuration Dependency!

Setting up configuration settings in a Rails app has been fairly straightforward for a while now:

# config/environments/development.rb
Doit::Application.configure do
  config.default_creator = "Person 1"
end

# config/environments/test.rb
Doit::Application.configure do
  config.default_creator = "Test Person"
end

# config/environments/production.rb
Doit::Application.configure do
  config.default_creator = "John Doe"
end

To access this setting, call Doit::Application.config.default_creator within your app. Pretty straightforward, right?

class TodoItem < ActiveRecord::Base
  before_create :assign_default_creator, unless: :creator?

  private

  def assign_default_creator
    self.creator = Doit::Application.config.default_creator
  end
end

Let's imagine you have a todo item and you want to test that the value gets assigned if no creator is provided.

Imagine how you'd test this.

describe TodoItem do
  it "assigns the default creator when no creator is assigned" do
    Doit::Application.config.stub(:default_creator).and_return("default creator from config")
    subject.save
    subject.creator.should == "default creator from config"
  end

  it "does not assign the default creator if it has been set" do
    Doit::Application.config.stub(:default_creator).and_return("default creator from config")
    subject.creator = "Jane Doe"
    subject.save
    subject.creator.should == "Jane Doe"
  end
end

There's a few things that are gross. First, we're referencing Doit::Application in the model spec. Second, we're stubbing in both examples. The latter is a low-hanging fruit so it can be extracted.

describe TodoItem do
  it "assigns the default creator when no creator is assigned" do
    config_default_creator_returns("default creator from config")
    subject.save
    subject.creator.should == "default creator from config"
  end

  it "does not assign the default creator if it has been set" do
    config_default_creator_returns("default creator from config")
    subject.creator = "Jane Doe"
    subject.save
    subject.creator.should == "Jane Doe"
  end

  def config_default_creator_returns(value)
    Doit::Application.config.stub(:default_creator).and_return(value)
  end
end

Now, interacting with Doit::Application is confined to one method. Some people may stub the application in an RSpec before block, but I don't like doing that because one of the specs cares about the stubbed value. I want that stub right in the example so it's obvious that the stub and assertion are close (in number of lines).

Even though Doit::Application is confined to one call in the spec, I really don't like that the spec cares about its config at all. What I'd love to do is assign a custom configuration on my TodoItem in my test intsead of caring about Doit::Application and having to stub on config. I can do this with dependency injection.

Right now, TodoItem has a dependency on Doit::Application.config. Dependency injection would mean TodoItem gets a class_attribute :config that defaults to Doit::Application.config but can be overridden (say, in our tests).

describe TodoItem do
  it "assigns the default creator when no creator is assigned" do
    subject.config = stub("config", default_creator: "default creator from config")
    subject.save
    subject.creator.should == "default creator from config"
  end

  it "does not assign the default creator if it has been set" do
    subject.config = stub("config", default_creator: "default creator from config")
    subject.creator = "Jane Doe"
    subject.save
    subject.creator.should == "Jane Doe"
  end
end

With the class attribute, I'm able to override config on the instance and replace it with a stub that has a default_creator method, which I've assigned to the string I expect. I was able to remove my reference of Doit::Application from the spec. Perfect!

Here's the model code:

class TodoItem < ActiveRecord::Base
  class_attribute :config
  self.config = Doit::Application.config

  before_create :assign_default_creator, unless: :creator?

  private

  def assign_default_creator
    self.creator = config.default_creator
  end
end

The callback assign_default_creator now doesn't care about Doit::Application.config, only that config has a default_creator method.

This post actually stemmed from my interaction with a developer in Factory Girl GitHub Issues who asked a pretty interesting question about reloading classes in Factory Girl after removing the constant and loading the Ruby file again (it seemed like a code smell and not an issue with Factory Girl).

At the end of the thread, I suggested he attend my Test-Driven Rails workshop next week, January 30th and 31st, because I'll be talking about RSpec and dependency injection (among other things like Cucumber, how, when, and what to test in a Rails app). It's perfect for Rails developers who are interested in writing more cleaner, more stable applications.

See you there!