views:

359

answers:

4

Here is what I have:

   context "Create ingredient from string" do
      context "1 cups butter" do

         setup do
            @ingredient = Ingredient.create(:ingredient_string => "1 cups butter")
         end

         should "return unit" do
            assert_equal @ingredient.unit, 'cups'
         end

         should "return amount" do
            assert_equal @ingredient.amount, 1.0
         end

         should "return name" do
            assert_equal @ingredient.name, 'butter'
         end
      end
      context "1 (18.25 ounce) package devil's food cake mix with pudding" do

         setup do
            @ingredient = Ingredient.create(:ingredient_string => "1 (18.25 ounce) package devil's food cake mix with pudding")
         end

         should "return unit" do
            assert_equal @ingredient.unit, '(18.25 ounce) package'
         end

         should "return amount" do
            assert_equal @ingredient.amount, 1.0
         end

         should "return name" do
            assert_equal @ingredient.name, 'devil\'s food cake mix with pudding'
         end
      end
   end

Clearly there is a lot of duplication there. Any thoughts on how to remove it, if only at the very least the context and the string?

A: 

Personally for this test, I wouldn't use Shoulda. You can easily remove duplication by using dynamic method creation as follows:

class DefineMethodTest < Test::Unit::TestCase
    [{:string => '1 cups butter', :unit => 'cups', :amount => 1.0, :name => 'butter'},{:string => '1 (18.25 ounce) package devil's food cake mix with pudding', :unit => '(18.25 ounce) package', :unit => 1.0, :name => "devil's food cake mix with pudding"}].each do |t|
        define_method "test_create_ingredient_from_string_#{t[:string].downcase.gsub(/[^a-z0-9]+/, '_')}" do
            @ingredient = Ingredient.create(:ingredient_string => t[:string])

            assert_equal @ingredient.unit, t[:unit], "Should return unit #{t[:unit]}"
            assert_equal @ingredient.amount, t[:amount], "Should return amount #{t[:amount]}"
            assert_equal @ingredient.name, t[:name], "Should return name #{t[:name]}"
        end
    end
end
Andrew
Although this is a slick use of dynamic methods I find this test much less readable the either Shoulda implementation.
Mike Breen
+2  A: 

Duplication in tests is not necessarily a Bad Thing(tm)

I suggest you read the following articles from Jay Field

http://blog.jayfields.com/2007/06/testing-one-assertion-per-test.html

http://blog.jayfields.com/2008/05/testing-duplicate-code-in-your-tests.html

They make a convinving case for code duplication in the tests and keeping one assertion per test.

Jean
Yeah, your main goal with tests (well other than testing your code) should be readability. The developer who has to maintain your code will thank you for it.
Mike Breen
+4  A: 

Here's a solution to your specific problem. The idea is to create a class method (like Shoulda's context, setup and should).

Encapsulate the repetition in a class method accepting all varying parts as arguments like this:

def self.should_get_unit_amount_and_name_from_string(unit, amount, name, string_to_analyze)
  context string_to_analyze do
    setup do
      @ingredient = Ingredient.create(:ingredient_string => string_to_analyze)
    end

    should "return unit" do
       assert_equal @ingredient.unit,   unit
    end

    should "return amount" do
       assert_equal @ingredient.amount, amount
    end

    should "return name" do
       assert_equal @ingredient.name,   name
    end
  end
end

Now you can call all these encapsulated tests with one liners (5-liners here for readability ;-)

context "Create ingredient from string" do
  should_get_unit_amount_and_name_from_string(
    'cups',                   
    1.0, 
    'butter', 
    "1 cups butter")
  should_get_unit_amount_and_name_from_string(
    '(18.25 ounce) package',  
    1.0, 
    'devil\'s food cake mix with pudding', 
    "1 (18.25 ounce) package devil's food cake mix with pudding")
end

In some cases, you may want to accept a block which could serve as your Shoulda setup.

webmat
With the latest release of [Shoulda][1] you can put you own macros in [RAILS_ROOT]\test\shoulda_macros and Shoulda will automagically pick them up. [1]: http://giantrobots.thoughtbot.com/2008/9/30/shoulda-2-0
Mike Breen
+1  A: 

Tests/specs are not production code and so being dry is not a priority.

The principle is that the specs should be clear to read, even if it means there is duplication of text across tests.

Don't be too concerned about specs being dry. Overemphasis of dry tests tends to make things more difficult as you have to jump around to the definitions of things to understand what is happening.

fatgeekuk