Let's start with a class** (CellPhone) containing a dependency (SimCard):
class CellPhoneIn 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.
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
class CellPhoneThis 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.
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
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::TestCaseThis 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.
def test_save_number
phone = CellPhone.new.save_number('555-1212')
assert phone.include_number?('555-1212')
end
end
class CellPhoneTest < Test::Unit::TestCaseAs you can see in the example the CellPhone class tests are no longer brittle despite the dependency on SimCard.
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
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'For more information check out the documentation for Mocha.
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
** 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
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...
ReplyDeletedef 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