Keep Your Friends Close, But Your Test Data Closer
Patrick Reagan, Former Development Director
Article Category:
Posted on
Like any Rails developer, you've been indoctrinated into the cult of DRY and are constantly removing duplication whenever you can as you add new functionality to your application. Refactoring is an important part of the development process and improves the maintainability and understandability of your application's code.
While this is good practice for production code, the tests in your application can benefit from refactoring as well. Often, it is the setup phase of the unit testing cycle where you will encounter the most duplication. The following example was extracted from Tweets of Fury (go play now, I'll wait) – I'm using both Shoulda and Matchy for the tests:
context "An instance of the Round class" do should "know that there hasn't been a response from the challenger" do user = Factory(:user) battle = Factory(:battle, :challenger => user) weapon = Factory(:weapon) round = battle.rounds.create!(:challenger_weapon => nil, :opponent_weapon => weapon) round.response_from?(user).should be(false) end should "know that the challenger has responded" do user = Factory(:user) battle = Factory(:battle, :challenger => user) weapon = Factory(:weapon) round = battle.rounds.create!(:challenger_weapon => weapon, :opponent_weapon => nil) round.response_from?(user).should be(true) end end
A quick refactoring might result in moving the duplication into the setup:
context "An instance of the Round class" do setup do @user = Factory(:user) @battle = Factory(:battle, :challenger => @user) @weapon = Factory(:weapon) end ... end
The tests can now become more focused once the setup code has been extracted:
should "know that there hasn't been a response from the challenger" do round = @battle.rounds.create!(:opponent_weapon => @weapon) round.response_from?(@user).should be(false) end should "know that the challenger has responded" do round = @battle.rounds.create!(:challenger_weapon => @weapon) round.response_from?(@user).should be(true) end
Better, but not quite what I want. I've removed the "noise" and can now better focus on the code under test, but it presents me with another issue: the setup that I need to perform is missing some information that is important to keep within the test itself. I want it to be clear to anyone reading either of these tests that the user is the challenger in this particular battle, especially as I continue to add tests for this model.
In order to make this state apparent and remove duplication, I created a helper method that handles the task of setting up the data for a round:
class RoundTest < ActiveSupport::TestCase def create_round_with_battle(round_options = {}, battle_options = {}) ... end end
The implementation isn't particularly important – what it allows me to do is keep the important setup data within my tests:
context "An instance of the Round class with a challenger" do setup { @challenger = Factory(:user) } should "know that there hasn't been a response from the challenger" do round = create_round_with_battle({:weapon_for => :opponent}, {:challenger => @challenger}) round.response_from?(@challenger).should be(false) end should "know that the challenger has responded" do round = create_round_with_battle({:weapon_for => :challenger}, {:challenger => @challenger}) round.response_from?(@challenger).should be(true) end end
To me, this strikes the proper balance. It gives me only the context I need while still giving me the required control over the initial state of the object under test. At this point, it may be tempting to tuck the create_round_with_battle method into test_helper.rb, but you should resist that urge. This method is only needed in this test, so it should remain here.