Testing in Rails: Part 8 - Validations

This is part of an ongoing series of posts about how to get started writing tests for Ruby on Rails. The series begins with the introduction and overview of the ideas behind testing.

Now that we have unit tests to insure that our models are related properly, we are ready to test our the validations in our models.

There are two approaches to testing your validations. It will be useful to examine both techniques because it will help you to write better tests overall. First, we need to write tests that confirm the code we have in place is working properly—as we have done thus far. Second, we need to think about special cases that we might not have coded into our validations already. This is where writing tests can really improve your code, and validations are a great place to see it in action.

Model Relationships and Validations Review

As a quick refresher, here are the relationships and validations of our three models.

1
2
3
4
5
6
7
8
9
10
11
12
class Wine < ActiveRecord::Base

  belongs_to :winery
  has_and_belongs_to_many :varietals
 
  validates_presence_of :name, :year, :family
  validates_length_of :name, :maximum => 100
  validates_format_of :year, :with => /\d{4}/, :message => 'must be a four-digit number'
  validates_length_of :family, :maximum => 255

  ...
end
1
2
3
4
5
6
7
8
9
10
11
12
13
class Winery < ActiveRecord::Base
 
  has_many :wines, :order => 'name ASC'
 
  validates_presence_of :name
  validates_uniqueness_of :name
  validates_length_of :name, :maximum => 255
  validates_length_of :city, :maximum => 255
  validates_length_of :state, :maximum => 255
  validates_length_of :country, :maximum => 255
 
  ...
end
1
2
3
class Varietal < ActiveRecord::Base
  has_and_belongs_to_many :wines
end

Manual Validation Testing

Think for a moment about how you test your validations without unit tests. You might fill out a form with bad data and make sure validation fails. Then you might fill out the form with good data and make sure it passes all validations. Or you might create an object in the console and then use the valid? method. Like this:

1
2
3
4
5
6
7
8
9
10
11
> ruby script/console

> test_wine = Wine.new(:name => 'Valid Wine', :year => 2008, :family => 'Merlot')

> test_wine.valid?
=> true

> test_wine.name = nil

> test_wine.valid?
=> false

Thankfully, unit tests will be much less tedious than filling out forms over and over. And much more reliable. In fact, once you know how to write tests, any time you find yourself filling out forms to test a little bell should go off in your head telling you that your time would be better spent writing tests as code. But the technique for testing validity in unit tests is the basically the same as the old way—does good data pass and bad data fail?

Assert Valid

We already have data in our fixtures which should be valid, so we can start by testing that. You can put all your validation into one test method or you can give every single one its own method. Both will work—it depends on your personal preference. In my example, I will group all of the validates_presence_of tests into one method and put other validation tests in separate methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WineTest < Test::Unit::TestCase
  fixtures :wines, :wineries, :varietals, :varietals_wines

  def setup
    @sunnyside = wineries(:sunnyside)
    @merlot = wines(:merlot)
    @pinot_noir = wines(:pinot_noir)
    @bordeaux = wines(:bordeaux)
  end
 
  def test_validates_presence_of
    #validates_presence_of :name, :year, :family
   
    # Test that the initial object is valid
    assert(@merlot.valid?)
  end
end

@merlot should be valid and our first test should pass. With that one line and our fixtures, it is as if we took the time to fill out a whole form with valid data. Much easier, eh?

There is another way to write that assertion. Instead of using assert(@merlot.valid?), we could use assert_valid(@merlot). If you look up the Rails source code, you will find that the second assertion is simply a method that calls assert(@merlot.valid?). It is just in a prettier wrapper.

Now we need to test cases where the validation should fail; the same as if we had filled out a form with bad data to make sure it did not pass validations. Let’s do that by setting the name to nil.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class WineTest < Test::Unit::TestCase
  ...
 
  def test_validates_presence_of
    #validates_presence_of :name, :year, :family
   
    # Test that the initial object is valid
    assert_valid(@merlot)
   
    # Test that it becomes invalid by removing the name
    @merlot.name = nil
    assert(!@merlot.valid?)
  end
end

Easy enough. It is very similar to the console code shown earlier.

Assert Not Valid

You might be wondering if there is a shorthand method similar to assert_valid. There is not one built in, but it is easy to write our own.

Put this method in test/test_helper.rb.

1
2
3
4
def assert_not_valid(object, msg="Object is valid when it should be invalid")
    assert(!object.valid?, msg)
  end
  alias :assert_invalid :assert_not_valid

Now you can utilize the convenience methods assert_not_valid and assert_invalid throughout your tests.

Validity Errors

There is something we overlooked while testing the invalid objects. Chances are, if we were submitting a bad form to test validations, we would also be watching for the errors returned to the form (error_messages_for). If we left the name field empty, we would expect to get an error saying “Name can’t be blank.” It is not just an indication that the test passed, it is an important part of the application’s behavior. What if the validation fails but the error message does not show up? If we were “form testing” we would spot that as a problem right away. We should include it in our unit tests.

1
2
3
4
5
6
7
8
9
10
11
def test_validates_presence_of
    #validates_presence_of :name, :year, :family
 
    # Test that the initial object is valid
    assert_valid(@merlot)
 
    # Test that it becomes invalid by removing the name
    @merlot.name = nil
    assert_invalid(@merlot)
    assert(@merlot.errors.invalid?(:name), "Expected an error on validation")
  end

Run the tests from the command line with ruby test/unit/wine_test.rb. They should all pass. Congratulations, you have just written your first set of validation tests!

Testing Validation of Multiple Attributes

What about testing the other validates_presence_of fields (:year and :family). If we were to put those tests in a separate method then setup would be run anew and @merlot will be valid at the start of the test. But since we are putting them all in the same method we will need to retain and restore the value of :name. Otherwise, our object would continue to be invalid. Once we do, we can write nearly identical tests for the :year and :family attributes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def test_validates_presence_of
    #validates_presence_of :name, :year, :family
 
    # Test that the initial object is valid
    assert_valid(@merlot)
 
    # Test that it becomes invalid by removing the name
    temp = @merlot.name
    @merlot.name = nil
    assert_invalid(@merlot)
    assert(@merlot.errors.invalid?(:name), "Expected an error on validation")
   
    # Make object valid again
    @merlot.name = temp

    # Test that the initial object is valid
    assert_valid(@merlot)
 
    # Test that it becomes invalid by removing the year
    temp = @merlot.year
    @merlot.year = nil
    assert_invalid(@merlot)
    assert(@merlot.errors.invalid?(:year), "Expected an error on validation")
   
    # Make object valid again
    @merlot.year = temp

    # Test that the initial object is valid
    assert_valid(@merlot)
 
    # Test that it becomes invalid by removing the family
    temp = @merlot.family
    @merlot.family = nil
    assert_invalid(@merlot)
    assert(@merlot.errors.invalid?(:family), "Expected an error on validation")
  end

Run the tests and make sure they all pass.

(Another approach would be to use @merlot.clone to get a copy that can be made invalid while leaving a valid object for future tests. Use whichever you prefer.)

DRY with Custom Test Helpers

Hopefully you have enough experience with programming that repeating code three times made you wince. DRY—don’t repeat yourself. It would be much better to abstract it into a method that we can call three times. To do this we will need to make use of the Ruby method send. It is a somewhat advanced method but a useful one to have in your programming toolbox. Here is how it works:

1
2
3
4
5
6
7
8
9
# The methods:
@merlot.name
# and
@merlot.name = nil

# are exactly the same as:
@merlot.send("name")
# and
@merlot.send("name=", nil)

Basically, we are “sending” the object a “message” which is the method name as a string (or as a symbol) with any arguments following it. The object will execute the message as a method call. This is useful when the method name being called is dynamic.

Using this technique we can write a helper method and put it in test/test_helper.rb.

1
2
3
4
5
6
7
8
9
10
11
12
13
def assert_presence_required(object, field)
    # Test that the initial object is valid
    assert_valid(object)
   
    # Test that it becomes invalid by removing the field
    temp = object.send(field)
    object.send("#{field}=", nil)
    assert_invalid(object)
    assert(object.errors.invalid?(field), "Expected an error on validation")
   
    # Make object valid again
    object.send("#{field}=", temp)
  end

Now that we have moved all the heavy lifting into a helper method, our unit tests become very simple:

1
2
3
4
5
6
def test_validates_presence_of
    #validates_presence_of :name, :year, :family
    assert_presence_required(@merlot, :name)
    assert_presence_required(@merlot, :year)
    assert_presence_required(@merlot, :family)
  end

Try to construct reusable helper methods like these to speed up test writing and development. You will get the extra benefit of making code easier to read. (If you like, you can take this particular helper method one step further so that it accepts either a single attribute, as shown, or an array of attributes.)

Testing validates_length_of

All validation tests work pretty much the same. Make sure a valid object passes, make sure invalid objects do not pass, make sure the error messages get added.

Let’s try validates_length_of. Since we know how beneficial it is to write helper methods, we will plan to use one from the start.

Think about validates_length_of for second. A string longer than the maximum length should fail validations. A string below that length, or even right at it, should pass. Remember from basic Ruby that “x” * 5 == “xxxxx”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def assert_required_length_less_than(object, field, length)
    dup_object = object.clone

    # Valid at length-1
    dup_object.send("#{field}=", "a"*(length-1))
    assert_valid(dup_object)

    # Valid at length
    dup_object.send("#{field}=", "a"*length)
    assert_valid(dup_object)

    # Invalid at length+1
    dup_object.send("#{field}=", "a"*(length+1))
    assert_invalid(dup_object)
    assert(dup_object.errors.invalid?(field), "Expected an error on validation")
  end

You will notice that this time I am using the “clone method” on the object passed in so that I do not alter the original object. If you wanted, you could use this as a starting point to write a helper method that would also allow testing minimum lengths and lengths within a range.

Now we just have to make use of our new helper method in our unit tests.

1
2
3
4
5
6
7
def test_validates_length_of
    #validates_length_of :name, :maximum => 100
    assert_required_length_less_than(@merlot, :name, 100)
 
    #validates_length_of :family, :maximum => 255
    assert_required_length_less_than(@merlot, :family, 255)
  end

You can see how easy it becomes to write basic validation tests, especially if you write helper methods that you can reuse.

Testing validates_format_of

Testing validates_format_of requires a different testing approach. In my opinion, the wide variations that can go into a format statement make it hard to write a generic test helper. Instead, we will need to plug-in our own test cases.

Our Wine model validates the format of the year using a regular expression. In regular expressions ‘\d’ indicates a digit 0-9. Putting curly braces with a number inside indicates how many times to repeat that. So ‘\d{4}’ simply means four digits that are 0-9.

We will start with what seems obvious: that @merlot is valid and that setting :year to a four-digit number will still be valid.

1
2
3
4
5
6
7
8
def test_validates_format_of
    #validates_format_of :year, :with => /\d{4}/
    assert_valid(@merlot)

    # example cases that should pass
    @merlot.year = 2008
    assert_valid(@merlot)
  end

Those two tests prove the positive case, and it got us started on the right track.

Now think of more examples that should pass. Then, for the negative tests, think of examples that should fail. Plug those in as tests and try them out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def test_validates_format_of
    #validates_format_of :year, :with => /\d{4}/
    assert_valid(@merlot)
 
    # example cases that should pass
    @merlot.year = 2008
    assert_valid(@merlot)
    @merlot.year = 0001
    assert_valid(@merlot)
    @merlot.year = 9999
    assert_valid(@merlot)
 
    # example cases that should fail
    @merlot.year = 333
    assert_invalid(@merlot)
    @merlot.year = 12345
    assert_invalid(@merlot)
    @merlot.year = '\d{4}'
    assert_invalid(@merlot)
    @merlot.year = 'wxyz'
    assert_invalid(@merlot)
    @merlot.year = '1234'
    assert_invalid(@merlot)
  end

In the tests that should pass, I set year to the lowest and highest possible four-digit numbers. In tests that should fail, I put a three-digit number, a five-digit number, ‘\d{4}’ (on the off chance that I misunderstood the usage of ‘/\d{4}/’ when writing my regular expression), a string of four letters and a string of four numbers. Run the tests!

Oops. You will get a failure. Examine the line number in the failure message and you will see that 0001 was not valid. To Rails, 0001 becomes simply 1. So the lowest four-digit number we can actually have is 1000. We can rewrite our tests so that 1000 is an example that passes and 0001 is an example that should fail, but more importantly, we should consider if this behavior is acceptable for our application. If not, then we need to rework our validation until the test passes. In this case, we do not need to allow wines with years below 1000 so we can leave it and move on.

Amend your tests: 1000 should be valid and 0001 should be invalid. Run the tests again. Bam! Another failure.

This time the failure was that a year of 12345 passes when it should not. If you change that year to 123456789, it still passes. Why? Because our regular expression says that the format should include four-digits. It does not specify that those should be the only four digits. Testing has uncovered a bug in our regular expression! This is a problem that we cannot live with and we need to work on our regular expression until it passes. Modifying the regular expression in Wine to be ‘/^\d{4}$/’ will fix it. (’^’ indicates the start of the line, ‘$’ indicates the end of the line.) Now it will match exactly four digits and nothing more.

1
2
3
4
5
6
class Wine < ActiveRecord::Base
  ...
  validates_format_of :year, :with => /^\d{4}$/,
    :message => 'must be a four-digit number'
  ...
end

After fixing the model code, try running the tests again. Still yet another failure! This time it was ‘1234′ that passed when it should have failed. Rails will handle the conversion from a string to an integer for us. (Generally these values are form values that are received as strings.) This time, it was our testing logic that was flawed. So we should change our test to make sure that ‘1234′ is valid.

With that change, all the tests pass again. Our revised test method looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def test_validates_format_of
    #validates_format_of :year, :with => /\d{4}/
    assert_valid(@merlot)
 
    # example cases that should pass
    @merlot.year = 2008
    assert_valid(@merlot)
    @merlot.year = 1000
    assert_valid(@merlot)
    @merlot.year = 9999
    assert_valid(@merlot)
    @merlot.year = '1234'
    assert_valid(@merlot)
 
    # example cases that should fail
    @merlot.year = 333
    assert_invalid(@merlot)
    @merlot.year = 12345
    assert_invalid(@merlot)
    @merlot.year = 0001
    assert_invalid(@merlot)
    @merlot.year = '\d{4}'
    assert_invalid(@merlot)
    @merlot.year = 'wxyz'
    assert_invalid(@merlot)
  end

These last few examples are important. They illustrate how assumptions can creep into your code and into your tests and how testing can help expose them. They also demonstrate the second way to test. Instead of writing helpers that test general cases, write specific cases that should pass or fail. These two approaches are not mutually exclusive either. You can write an general helper test and plug in some example values.

Test Driven Development

It also introduces a new concept, Test Driven Development. Test Driven Development (often abbreviated as TDD) is a style of development where you write your tests first and then write code that passes your tests. Some experienced developers swear by it (I am not one), but it is difficult for beginners to get their head around.

In our previous example, imagine that you were really inexperienced at regular expressions. You know you need to use a validates_format_of but do not know how to write it. But you do know how to write unit tests. So you write a test method like the one we have above. You think of example cases that should validate and that should not validate. Then, once your tests are in place, you get out a book on regular expressions and start fumbling your way through it. It may be trial and error for you, but you will know that you have succeeded when all of your tests pass. If they do not pass, then you will know you have to keep trying and your tests will give you helpful clues as to why it is not working.

TDD is a similar process. The difference is that TDD is not just for one method, it is for all of your code. Tests drive your development. You write tests before you write code in any part of your application.

Beginners should focus on learning testing first. But keep TDD in your head as an option to try someday. You might find it suits your way of thinking about code.

Testing What You Did Not Code

The other important part of validation testing is stopping to consider what validations might be missing. While your model’s validations are in the front of your mind, make sure that you did not overlook some. You may have written the validations a long time ago and your ideas about what needs validating may be more refined now.

You might find it helpful to start a new test method, called something like test_validation_generally, where you can manufacture some crazy examples that should or should not pass. Then you could use a TDD approach to writing validation code that either allows or declines your examples.

Take a stab at writing the other validation tests on your own. In particular, try validates_uniqueness_of :name. Think about how you should test if the name is unique. You can try it with either a general helper method or by plugging in values that should pass and fail (or both).

UPDATE: The series continues in Part 9.

Bookmark and Share

9 Responses to “Testing in Rails: Part 8 - Validations”

  1. David Says:

    This is great and I can’t wait for more, but I think you have a typo in your text for the error in the validates format with regex. The code is right with the $ inside the / but the text shows them swapped. Great series.

    David

  2. Kevin Skoglund Says:

    Good catch. Thanks, David.

  3. Nicos Picos Says:

    Hey,

    I was wondering how come you don’t use such helper methods for testing relationships. Just spent an entire day writing relationship tests, and would have been a lot easier to just have templates (even for the setup could be nice to have a template that will set up all your fixtures according to parameters).

    Any reason not to do this?

  4. Kevin Skoglund Says:

    There is no reason not to use helpers anytime you are repeating yourself. They make testing a lot easier. I didn’t use them through this tutorial because I wanted to make sure that the core concepts were clear.

  5. Nicos Picos Says:

    I recommend for the helper that tests presence_of to use clone objects, because otherwise an error will be thrown in the reference for temp. In your example it works because you are just checking for the presence of literals. I found I was usually testing for the presence of other objects. Here is the edited code I used:

    # helper method used for validates_presence of
    def assert_presence_required(object, field)

    # Test that the initial object is valid
    assert_valid(object)

    # Test that it becomes invalid by removing the field
    temp = object.send(field)

    # CHANGE = use a clone because otherwise reference to the temp object is lost
    tempClone = temp.clone
    object.send(”#{field}=”, nil)
    assert_invalid(object)
    assert(object.errors.invalid?(field), “Expected an error on validation”)

    # CHANGE = set it to the clone now to make object valid again.
    object.send(”#{field}=”, tempClone)
    assert_valid(object)

  6. Nicos Picos Says:

    Man.. really wish I had used helpers for the relationship stuff. Thanks for this tutorial, there is little good stuff on testing out there - and I found this very thorough and well written!

  7. David T. Says:

    I guess

    alias :assert_invalid :assert_not_valid

    should have been

    alias :assert_invalid, :assert_not_valid

    with a comma.

  8. ujihisa Says:

    alias isn’t a class method but a instruction. That comma is not needed :-)

  9. sk Says:

    Hi
    For winery_test.rb I wrote
    assert_required_length_less_than(@sunnyside,:name,255)
    assert_required_length_less_than(@sunnyside,:city,255)
    assert_required_length_less_than(@sunnyside,:state,255)
    assert_required_length_less_than(@sunnyside,:country,255)

    But when run the tests i get failure
    [/test/test_helper.rb:52:in `assert_required_length_less_than'
    /test/unit/winery_test.rb:25:in `__bind_1243421822_280098']:
    Errors: name has already been taken (”Sunnyside Vineyards”).
    is not true.
    Why this happens?
    sk

Leave a Reply