Topic: Test Helpers: Refactoring a Validation Test

If you currently aren't testing your code, you really should look into it. Rails makes it fairly easy to do unit testing, and once you get rolling you'll wonder how you lived without it.

Sometimes unit tests can get rather ugly and have a lot of duplication. We've heard about this DRY thing, but does it really apply to test code as well? If you keep your tests DRY through refactoring, you'll start to build a library of helpers which can make testing so much easier. We are going to look into one such refactoring in this article.

First a little background, we are building a Cart engine, and we have a LineItem model with a quantity attribute. Seems pretty normal. In our test we want to make sure the quantity attribute doesn't accept invalid values. So, here's a test case that tries a few invalid values and makes sure they fail:

def test_invalid_quantity
  item = LineItem.new(:quantity => -1)
  assert !item.valid?
  assert item.errors.invalid?(:quantity)
  item = LineItem.new(:quantity => 'foo')
  assert !item.valid?
  assert item.errors.invalid?(:quantity)
  item = LineItem.new(:quantity => 2.5)
  assert !item.valid?
  assert item.errors.invalid?(:quantity)
end

The test is all well and good, but there is a lot of duplication. An object is being created with a new quantity value each time, and tested to make sure the quantity is invalid. Time to remove the duplication.

Where do we start? This seems like a good place to add a new assertion method. I like to write the interface of the method before actually adding the method because it forces me to think about readability. Let's make a simple assertion to test an invalid value:

def test_invalid_quantity
  assert_invalid_value LineItem, :quantity, -1
  # ...
end

That looks decent. Now it's time to write the method. Where do we put it? For now, just include it in the same class. I'm just basically moving a slice of code from the original test into this new assertion method and using variables instead of the static values. It looks like this:

def assert_invalid_value(model_class, attribute, value)
  record = model_class.new(attribute => value)
  assert !record.valid?
  assert record.errors.invalid?(attribute)
end

Let's use that method for all three checks and our test ends up looking like this:

def test_invalid_quantity
  assert_invalid_value LineItem, :quantity, -1
  assert_invalid_value LineItem, :quantity, 'foo'
  assert_invalid_value LineItem, :quantity, 2.5
end

Hmm, still some duplication. If we allow the assertion method to accept an array of values, that will remove the duplication.

def test_invalid_quantity
  assert_invalid_value LineItem, :quantity, [-1, 'foo', 2.5]
end

def assert_invalid_value(model_class, attribute, value)
  if value.kind_of? Array
    value.each { |v| assert_invalid_value model_class, attribute, v }
  else
    record = model_class.new(attribute => value)
    assert !record.valid?
    assert record.errors.invalid?(attribute)
  end
end


That looks really good. We can make the assertions a little more descriptive by adding messages to them. Like so:

def assert_invalid_value(model_class, attribute, value)
  if value.kind_of? Array
    value.each { |v| assert_invalid_value model_class, attribute, v }
  else
    record = model_class.new(attribute => value)
    assert !record.valid?, "#{model_class} expected to be invalid when #{attribute} is #{value}"
    assert record.errors.invalid?(attribute), "#{attribute} expected to be invalid when set to #{value}"
  end
end

Hey, this method seems really useful so I want to make it available to all my test cases. Well, it just so happens there's a file called test_helper.rb in your test directory which is made for just that! Simply move the method into the class there and you're done.

I can think of another way to make this assert method even more valuable. The interface could look something like this:

assert_invalid_attributes LineItem, :quantity => [-1, 'foo', 2.5], :product => nil

This would allow you to test multiple attributes! For now I'll leave creating this method as an exercise for the reader. I may post my implementation a little later.

Last edited by ryanb (2006-08-23 13:35:35)

Railscasts - Free Ruby on Rails Screencasts

Re: Test Helpers: Refactoring a Validation Test

Nice work.  I can already think of a hundred places to use something like this.

Re: Test Helpers: Refactoring a Validation Test

That's really practical, thanks.

Re: Test Helpers: Refactoring a Validation Test

Enhancement: http://pragmatig.wordpress.com/2008/04/ … e-dry-way/

working with a set of valid attributes, so that things like:
username should not be the same as password, password should match password_confirmation ... can be tested