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 Bot GitHub Issues who asked a pretty interesting question about reloading classes in Factory Bot after removing the constant and loading the Ruby file again (it seemed like a code smell and not an issue with Factory Bot).
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!