Friday, August 11, 2006

Alternative to Dependency Injection

Generally, when a class has a dependency it is preferred to use Inversion of Control. The popular methods for specifying the dependency are setter and constructor injection. Dependency Injection is one of my favorite patterns because it facilitates loosely coupled code that is easily testable. However, recently I found an alternative solution that can be used when DI is used solely for testing.

Let's start with a class** (CellPhone) containing a dependency (SimCard):
class CellPhone
def initialize
@sim_card = SimCard.new
end

def save_number(number)
@sim_card.save_number(number)
end

def include_number?(number)
@sim_card.include_number?(number)
end
end
In this example it is not clear how you could test the CellPhone class without depending on the SimCard class. This can create problems such as the SimCard class raising errors like SimCardFullError when you are actually only trying to test the CellPhone class. A simple solution is to introduce Constructor Injection to decouple the SimCard class.
class CellPhone
def initialize(sim_card)
@sim_card = sim_card
end

def save_number(number)
@sim_card.save_number(number)
end

def include_number?(number)
@sim_card.include_number?(number)
end
end
This solution does work; however, you now need to create an instance of the SimCard class before you can create an instance of the CellPhone class. In the case of this example additional work has been created because the CellPhone never needs to use different types of SimCards except when testing.

There is an alternative solution that allows you to stub the behavior of a class without stubbing the entire class. This alternative solution is available because Ruby allows you to re-open classes. To see how this works we will need a test.
class CellPhoneTest < Test::Unit::TestCase
def test_save_number
phone = CellPhone.new.save_number('555-1212')
assert phone.include_number?('555-1212')
end
end
This test will pass with the first implementation assuming the SimCard class does not cause a problem. The change to test using the second implementation is straightforward enough that I don't feel it's necessary to demonstrate. However, what I do find interesting is how I can test CellPhone in isolation without changing the first implementation. The way to achieve this is to re-open the class within the test and alter the behavior of the class.
class CellPhoneTest < Test::Unit::TestCase
def setup
class << SimCard
alias :save_old :save_number
alias :include_old :include_number?
def save_number(number)
end

def include_number?(number)
true
end
end
end

def teardown
class << SimCard
alias :save_number :save_old
alias :include_number? :include_old
end
end

def test_save_number
phone = CellPhone.new.save_number('555-1212')
assert phone.include_number?('555-1212')
end
end
As you can see in the example the CellPhone class tests are no longer brittle despite the dependency on SimCard.

It's important to note that for our example it is assumed that CellPhone will always depend on the SimCard class. If it needed to depend on different types of SimCards then Constructor Injection would be necessary anyway.

While our solution does work it's not entirely elegant. Luckily, James Mead pointed me at Mocha, a library that facilitates this type of behavior switching. Using Stubba the test becomes much cleaner.
require 'stubba'

class CellPhoneTest < Test::Unit::TestCase
def test_save_number
SimCard.stubs(:new).returns(stub_everything(:include_number? => true))
phone = CellPhone.new.save_number('555-1212')
assert phone.include_number?('555-1212')
end
end
For more information check out the documentation for Mocha.



** Of course, the real class def would be
class CellPhone
extend Forwardable

def_delegators :@sim_card, :save_number, :include_number?

def initialize
@sim_card = SimCard.new
end
end

1 comment:

  1. Anonymous5:45 PM

    I'm glad you've found Stubba useful. You could further simplify your test by doing away with the hard-coded SimCardStub either like this...

    def test_save_number
    sim_card = stub(:save_number => nil, :include_number? => true)
    SimCard.stubs(:new).returns(sim_card)
    phone = CellPhone.new.save_number('555-1212')
    assert phone.include_number?('555-1212')
    end

    ... or like this ...

    def test_save_number
    SimCard.any_instance.stubs(:save_number)
    SimCard.any_instance.stubs(:include_number?).returns(true)
    phone = CellPhone.new.save_number('555-1212')
    assert phone.include_number?('555-1212')
    end

    ReplyDelete

Note: Only a member of this blog may post a comment.