January 07, 2009
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
Jan 08, 2009
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
Jan 08, 2009
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.
Jan 08, 2009
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
?
Jan 08, 2009
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
Jan 08, 2009
@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?
Jan 08, 2009
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.
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).
Added benefit: these tests don’t require fixtures or hit the database.
Jan 08, 2009
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
Jan 08, 2009
I want to mention Shoulda here one more time, because I really like those macros it brings to your tests:
Btw: John, you’re preview function is awesome!!
Jan 08, 2009
@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.
Jan 08, 2009
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. :)
Jan 08, 2009
@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.
Jan 08, 2009
Sorry to tell you this, but YAML does not stand for “Yet Another Markup Language”, but “YAML Ain’t a Markup Language”. ;)
Jan 08, 2009
@David – Haha. I wrote this late last night. Give me a break! Updated the article, thanks for pointing that out.
Jan 09, 2009
Inspired by this post, I wrote up a shoulda/factory_girl version
It would be awesome if someone did an rspec version.
Jan 09, 2009
Awesome Josh. Thanks for linking that up.
Jan 09, 2009
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.:
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?
Jan 09, 2009
@Lawrence – For most developers and most apps that will never be an issue.
Jan 09, 2009
@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.
Jan 09, 2009
@John so then you might as well not bother defining the unique index?
@Josh should_have_indices doesn’t allow a test for uniqueness.
Jan 10, 2009
@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.
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.
This is the error i got.
.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!
Jan 13, 2009
I just wanted to say that thanks to Lawrence, and thanks to this discussion, we added the following to two of the shoulda macros:
should_require_unique_attributes
macroshould_have_index
macrohttp://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.