This post originated from an RSS feed registered with Ruby Buzz
by Jay Fields.
Original Post: Testing: Replace Collaborator with Stub
Feed Title: Jay Fields Thoughts
Feed URL: http://feeds.feedburner.com/jayfields/mjKQ
Feed Description: Blog about Ruby, Agile, Testing and other topics related to software development.
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\d\d)(\d\d\d)(\d\d\d\d)/, '(\1) \2-\3') end end