January 07, 2009

Posted by John

Tagged test unit, testing, and validations

Older: My Testing Theory

Newer: HTTParty Meet Mr. Response

Test Or Die: Validates Uniqueness Of

In the test or die, I showed a simple example of how to test validates_presence_of. Let’s build on that by adding categories and then ensure that categories have a unique name that is not case sensitive. If you haven’t been following along and want to, go back to the beginning and create your app and first test. Let’s start by running the following commands to create the category model and migration, migrate your development database and prepare your test one.

script/generate model Category name:string
rake db:migrate
rake db:test:prepare

The important thing to remember when testing uniqueness of is that it does a check with the database to see if the record is unique or not. This means you need to have a record in the database to verify that the validation does in fact get triggered. You can do this several ways but since we are staying simple, we’ll do this in test/unit/category_test.rb:

require 'test_helper'

class CategoryTest < ActiveSupport::TestCase
  test 'should have unique name' do
    cat1 = Category.create(:name => 'Ruby')
    assert cat1.valid?, "cat1 was not valid #{cat1.errors.inspect}"
    
    cat2 = Category.new(:name => cat1.name)
    cat2.valid?
    assert_not_nil cat2.errors.on(:name)
  end
end

This breaks the one assertion per test that some people hold dear, but we are just learning right now. As you do this more and more, you will run into gotchas but by then you will know where to look for solutions. Now run rake to see if your test is failing.

$ rake
/opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/category_test.rb" "test/unit/post_test.rb" 
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
Started
F.
Finished in 0.280179 seconds.

  1) Failure:
test_should_have_unique_name(CategoryTest)
    [./test/unit/category_test.rb:10:in `test_should_have_unique_name'
     /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:94:in `__send__'
     /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:94:in `run']:
<nil> expected to not be nil.

2 tests, 3 assertions, 1 failures, 0 errors

Now that we have failed, let’s add the validation to our Category model.

class Category < ActiveRecord::Base
  validates_uniqueness_of :name
end

Run rake again and you will see happiness! Another way you could test the same thing above is by creating a fixture with a name of Ruby and then just use that fixture in place of cat1. The fixture file (test/fixtures/categories.yml) would look like this:

# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html

ruby:
  name: Ruby

Fixtures use yaml (YAML Ain’t Markup Language) to format the data. Each fixture has a name. In this case, we named our fixture ‘ruby’. In our test, we will access it using the same name.

require 'test_helper'

class CategoryTest < ActiveSupport::TestCase  
  test 'should have unique name' do
    </code><strong><code>ruby = categories(:ruby)</code></strong><code class="ruby">
    
    category = Category.new(:name => ruby.name)
    category.valid?
    assert_not_nil category.errors.on(:name)
  end
end

Note the bold line above. It uses the categories method to access the category fixture named ruby. It returns a category just like Category.find with an id would. The difference is that we don’t define id’s in our fixtures, so we don’t know what ruby’s id would be and, more importantly, names are often more intent revealing (think fixture names like active, inactive, published, not_published).

That was easy but we didn’t actually verify case insensitivity. Let’s add a bit more to make sure that is in fact the case.

require 'test_helper'

class CategoryTest < ActiveSupport::TestCase  
  test 'should have unique name' do
    ruby = categories(:ruby)
    
    category = Category.new(:name => ruby.name)
    category.valid?
    assert_not_nil category.errors.on(:name)
    
    </code><strong><code>category.name = ruby.name.downcase
    category.valid?
    assert_not_nil category.errors.on(:name)</code></strong><code class="ruby">
  end
end

Run rake again and FAIL! OH NOEZ! No worries, we just forgot to add the case sensitive part to the validation.

class Category < ActiveRecord::Base
  validates_uniqueness_of :name</code><strong><code>, :case_sensitive => false</code></strong><code class="ruby">
end

Let’s run rake one last time and make sure that things are passing.

$ rake
/opt/local/bin/ruby -Ilib:test "/opt/local/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader.rb" "test/unit/category_test.rb" "test/unit/post_test.rb" 
Loaded suite /opt/local/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
Started
..
Finished in 0.396569 seconds.

2 tests, 3 assertions, 0 failures, 0 errors

Yep, we are good to go. So how would you test this with a scope on the uniqueness validation? Well, I’m not going to add a column and all that but it would look something like this if the column you were scoping uniqueness to was site_id.

require 'test_helper'

class CategoryTest < ActiveSupport::TestCase  
  # ... same stuff as above
  
  test 'should not allow the same name for the same site' do
    ruby = categories(:ruby)
    
    category = Category.new(:name => ruby.name, :site_id => ruby.site_id)
    category.valid?
    assert_not_nil category.errors.on(:name)
  end
  
  test 'should allow the same name for different sites' do
    ruby = categories(:ruby)
    site = sites(:not_ruby_site)
    
    category = Category.new(:name => ruby.name, :site_id => site.id)
    category.valid?
    assert_nil category.errors.on(:name)
  end
end

That was pretty much off the top of my head, but it should give you the idea. The important thing is to test both sides. Don’t simply test that it is invalid for the same site, also test that it is valid for different sites. It may not be super important in this instance, but it is important to get into this mindset when testing.

You always want to think about the bounds. Test out of bounds on both sides and then in bounds to make sure that all cases are covered. Again, this is just the basics. There are other ways to do this, but I just thought I would get you pointed in the right direction.

22 Comments

  1. keep your validation tests dry:

    before {@user = valid User}

    @valid_attributes[:name]=users(:one).name
    assert_invalid_attributes User, :name=>[users(:one).name,nil,‘short’]

    User.name expected to be invalid when set to Mr. One

    http://github.com/grosser/valid_attributes/tree/master

  2. Don’t forget to set a unique index on any field(s) you have validates_uniqueness_of. Without a unique index in your database, you aren’t able to ensure true uniqueness, due to a race condition between checking uniqueness and saving your model.

    I always use both validates_uniqueness_of and a unique index.

  3. In practice, do you write out these sorts of tests over and over for each model, or do you abstract them out into methods. Seems like it should only take one line of code to do a check for common things like uniqueness checking a field/scoped field. Although at that point I start getting the creeping “writing the same code twice” feeling

    assert_validates_uniqueness_of :name, 
       :case_sensitive =&gt; false

    ?

  4. gnaa i pasted to much…

    assert_invalid_attributes User, :name=>[users(:one).name]

    is all you need…

    and if you want to ensure that its not case sensitive than do:

    assert_invalid_attributes User, :name=>[users(:one).name.downcase]

    valid attributes plugin

  5. @grosser – See at that point I start thinking it’s a violation of DRY to have that and the validates_uniqueness_of in the model – it seems to obviously be the same code just expressed in a different domain language

    I wonder if you could dynamically translate a certain subset of common unit tests directly into application code – e.g. a plugin at runtime to read test code and make appropriate modifications to the application’s classes. Well, actually I’m almost sure you could… I wonder if you should?

  6. These strategies are actually testing ActiveRecord, though, aren’t they? What I generally try to test is the piece that I’m responsible for, which in this case is just that the validation macro is entered correctly into the AR class definition.

    class CategoryTest &lt; ActiveSupport::TestCase
      test "should have unique name" do
        Category.expects(:validates_uniqueness_of).with(:name)
        load File.join(Rails.root, 'app', 'models', 'category.rb')
      end
    end
    

    The only downside to this is that you can’t build up the test suite piece-by-piece; if you add case-insensitivity, for instance, you have to change the existing test (instead of adding a new one).

    class CategoryTest &lt; ActiveSupport::TestCase
      test "should have case-insensitive unique name" do
        Category.expects(:validates_uniqueness_of).with(:name, :case_sensitive =&gt; false)
        load File.join(Rails.root, 'app', 'models', 'category.rb')
      end
    end
    

    Added benefit: these tests don’t require fixtures or hit the database.

  7. I like that approach, but is it getting too purist for it’s own good? If we take one of the goals of testing to be telling us when the app fails, even if its a failure in the DB schema or in Rails(because of an upgrade)

    Seems like a tough question… there’s advantages and disadvantages to either path

  8. I want to mention Shoulda here one more time, because I really like those macros it brings to your tests:

    
    class PostTest &lt; Test::Unit::TestCase
      should_belong_to :user
      should_have_many :tags, :through =&gt; :taggings
    
      should_require_unique_attributes :title
      should_require_attributes :body, :message =&gt; /wtf/
      should_require_attributes :title
      should_only_allow_numeric_values_for :user_id
    end
    

    Btw: John, you’re preview function is awesome!!

  9. @crayz: That is one of the goals of testing as a whole, but it’s not (my|the) goal for unit testing. I look for application-level failures due to the framework at the functional and/or integration levels.

  10. I’ll reiterate. On a regular basis, I do not test this way. I believe there is a natural progression that you go through when learning testing. You start with stuff like this. Then, you notice that you can make it easier on yourself and you start to create macros to test repetitive things.

    The thing that I have noticed when teaching people testing though is they have to learn this first. If they start with the macros, mocking and stubbing, they never quite understand the underpinnings. Also, when they start new project, I often watch them stumble, trying to do things with all the macros, but not having their test environment setup. I think it is good for people to see that they are repeating themselves and think, well, how can I fix this? I kind of think it is good to feel the repetition pain as it helps people understand why we switch to macros and start to stub and mock things so that you don’t require hitting the database or using fixtures.

    @Ben – The only thought on the other side of the argument I can come up with, because I mostly agree with you, is what if Rails changes underlying functionality that makes sense for Rails, but not for the business logic of your application.

    Let’s say you were using validates_uniqueness_of :name without the :case_sensitive option and Rails changes the default from true to false. I would want to know that in my tests as a simple upgrade of Rails would modify my business logic and cause nothing to fail using your methods. Does that make more sense? Again, I agree with you, just mentioning the other side of the coin.

    Also, I much like RSpec’s folder naming of controllers and models, as I think that Rails functional and unit are kind of misleading. I see Rail’s unit tests as model tests, not pure unit tests.

    @crayz – I definitely do make macros. I even make TextMate bundles on top of those macros to shorten things up even more.

    All that said, I’m glad everyone is commenting like this. It is good for people to realize that what I am showing is the start and that there are better ways, but sometimes you are still better of starting from the beginning. Hope this comment clears some stuff up. :)

  11. @crayz

    You are repeating yourself, but in the case of the repetition between your application code and testing code, it’s more acceptable.

    By saying what you want to test, and then implementing that functionality, you ensure you add the correct code.

    I’ve had plenty of times where I’ve screwed up the test code or the application code, but almost never both at the same time.

  12. Sorry to tell you this, but YAML does not stand for “Yet Another Markup Language”, but YAML Ain’t a Markup Language”. ;)

  13. @David – Haha. I wrote this late last night. Give me a break! Updated the article, thanks for pointing that out.

  14. Inspired by this post, I wrote up a shoulda/factory_girl version

    It would be awesome if someone did an rspec version.

  15. Awesome Josh. Thanks for linking that up.

  16. Nice discussion going on here.

    @ Seth Ladd : do you then also have a test to check that the unique index is indeed defined in the database? I.e. simulate a race condition, e.g.:

    
    test "should have a unique index on name" do
        cat = Category.new(:name =&gt; categories(:ruby).name.upcase)
        assert_raises(ActiveRecord::StatementInvalid) { cat.save(false) }
    end
    

    Issue is that when later new attributes are added to the model which are required this test will start to fail.

    Other suggestions?

    PS. Just peeked in the shoulda code, their macro doesn’t test for this race condition. Not important enough to test?

  17. @Lawrence – For most developers and most apps that will never be an issue.

  18. @Lawrence, @Seth: shoulda has should_have_indices which can test the presence of an index. Hit ‘view source’ to get an idea of how it’s tested under the hood.

  19. @John so then you might as well not bother defining the unique index?

    @Josh should_have_indices doesn’t allow a test for uniqueness.

  20. @Lawrence – I typically don’t define unique indexes at the database level. Not saying don’t but I haven’t needed to. The database check works fine.

  21. suman gurung suman gurung

    Jan 13, 2009

    I am a beginner in testing. I was trying out all the steps and i got an error when i changed the category model to the following and ran rake.

    
    class Category &lt; ActiveRecord::Base
      validates_uniqueness_of :name, case_sensitive =&gt; false
    end
    

    This is the error i got.

    
    E:\ROR\sources\first_test&gt;rake
    (in E:/ROR/sources/first_test)
    c:/ruby/bin/ruby -Ilib;test "c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/
    rake_test_loader.rb" "test/unit/category_test.rb" "test/unit/post_test.rb"
    c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.2.2/lib/active_record/base.rb:1833
    :in `method_missing': undefined local variable or method `case_sensitive' for #&lt;
    Class:0x47f6f08&gt; (NameError)
            from E:/ROR/sources/first_test/app/models/category.rb:2
            from c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `ge
    m_original_require'
            from c:/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:31:in `re
    quire'
            from c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_suppo
    rt/dependencies.rb:155:in `require'
            from c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_suppo
    rt/dependencies.rb:262:in `require_or_load'
            from c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_suppo
    rt/dependencies.rb:221:in `depend_on'
            from c:/ruby/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_suppo
    rt/dependencies.rb:133:in `require_dependency'
            from c:/ruby/lib/ruby/gems/1.8/gems/rails-2.2.2/lib/initializer.rb:368:i
    n `load_application_classes'
             ... 14 levels...
            from c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
    .rb:5:in `load'
            from c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
    .rb:5
            from c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
    .rb:5:in `each'
            from c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/rake_test_loader
    .rb:5
    c:/ruby/bin/ruby -Ilib;test "c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/
    rake_test_loader.rb"
    c:/ruby/bin/ruby -Ilib;test "c:/ruby/lib/ruby/gems/1.8/gems/rake-0.8.3/lib/rake/
    rake_test_loader.rb"
    Errors running test:units!

    .
    It worked fine for the other previous test, and gave the error only when i added the case to test the case sensitivity for the category name.

    Great series though, i am enjoying it!

  22. I just wanted to say that thanks to Lawrence, and thanks to this discussion, we added the following to two of the shoulda macros:

    • a :case_sensitive option to the should_require_unique_attributes macro
    • a :unique option to the should_have_index macro

    http://thoughtbot.lighthouseapp.com/projects/5807/tickets/130
    http://thoughtbot.lighthouseapp.com/projects/5807/tickets/129

    Thanks!

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.