Sunday, May 27, 2007

Testing: Replace Collaborator with Stub

If you've done much testing I'm sure you've been annoyed by cascading failures. Cascading failures are most often a symptom of tests that rely on concrete classes instead of isolating the class under test. A solution for creating more robust tests is to replace the collaborators with stubs.

Example: Testing the AccountInformationPresenter independent of the Account class.

The prefactored test involves testing the presenter by creating the Account ActiveRecord instance and testing that the presenter correctly formats the phone number of the instance.
class AccountInformationPresenterTest < Test::Unit::TestCase
def test_phone_number_is_formatted_correctly
Account.delete_all
Account.create(:phone_number => '1112223333')
account = Account.find :first
presenter = AccountInformationPresenter.new(account.id)
assert_equal "(111) 222-3333", presenter.phone_number
end
end
The potential issue with the above test is that if the Account class changes this test could fail. For example, if a validation is added to Account that verifies that the area code of the phone number is valid the above test will fail despite the fact that nothing about the presenter changed.

The same behavior of the presenter can be tested without relying on an actual instance of the Account class. The following test takes advantage of the Mocha mocking and stubbing framework. The new version of the test mocks the interaction with the Account class and returns a stub instead of an actual account instance.
class AccountInformationPresenterTest < Test::Unit::TestCase
def test_phone_number_is_formatted_correctly
Account.expects(:find).returns(stub(:phone_number => '1112223333'))
presenter = AccountInformationPresenter.new(:any_number)
assert_equal "(111) 222-3333", presenter.phone_number
end
end
The new test will not fail regardless of any changes to the Account class.

Where the refactoring applies
This refactoring is most often applicable to unit tests. The unit test suite is often a good place to test edge cases and various logic paths. However, there is much value in testing collaboration among the various classes of your codebase. For these reasons you wouldn't want to apply this refactoring to your functional tests. If you do apply this refactoring, you should ensure you have proper coverage in the functional suite.

The code for the AccountInformationPresenter remains the same regardless of test implementation and is only shown for completeness.
class AccountInformationPresenter
def initialize(account_id)
@account = Account.find(account_id)
end

def phone_number
@account.phone_number.gsub(/(\d{3})(\d{3})(\d{4})/, '(\1) \2-\3')
end
end
Post a Comment