advertisement

Print

Rails Testing: Not Just for the Paranoid
Pages: 1, 2, 3, 4, 5

What parts of my models need testing?

According to the folks who take test driven development seriously, the only features that need testing are the features that need to work. Though this is obviously a bit facetious, it's fairly close to the truth.



To break it down into a few categories, you will definitely want to test the following components of your models:

  • validations
  • search functions
  • model logic
  • anything else that does something interesting

The Entry model from Tasty has several components that fall into these categories:

app/models/entry.rb
class Entry < ActiveRecord::Base

  validates_uniqueness_of :url

  belongs_to :user
  has_many :taggings
  has_many :tags, :through => :taggings

  def created_date
    created_at.strftime("%Y.%m.%d")
  end

  def updated_date
    updated_at.strftime("%Y.%m.%d")
  end

 # Adds a tag with the given name, if it's not already present
  def tag_as(tagname)
    unless tagged_as?(tagname)
      tags << Tag.find_or_create_by_name(tagname)
    end 
  end

  # True if tags include a Tag with the given name, False otherwise
  def tagged_as?(tagname)
    tag_names.include?(tagname)
  end

  # returns a list of tag names
  def tag_names
    tags.map(&:name)
  end

  protected

  def validate
    if short_description =~ /rube goldberg/i
      errors.add("short_description", "can't include references to Rube")
    end
  end

end

We'll start with the validation tests, and then move on to some of the other functions in this model, showing several different tests and how they work.

Testing validations

Since the purpose of a validation is to maintain data integrity, it's very important to test that they work properly. If you think about it, one intuitive way of testing a validation is to attempt saving invalid data and then to ensure it is handled correctly. That's exactly what we'll do, so let's start by ensuring our unit tests know how to fail (proving that they're hooked up). We also won't be using fixtures for this set of tests, so you can remove that line too.

test/unit/entry_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class EntryTest < Test::Unit::TestCase
  def test_validates_unique_url
    flunk "Test failed as expected" 
  end
end

If all goes well, running rake test should give you something like this:

  1) Failure:
test_validates_unique_url(EntryTest) [./test/unit/entry_test.rb:5]:
Test failed as expected.

This proves that test_validates_unique_url is being called, which means we can replace it with a real test.

test/unit/entry_test.rb
require File.dirname(__FILE__) + '/../test_helper'

class EntryTest < Test::Unit::TestCase
  def test_validates_unique_url

    # Add an Entry to the DB so we have something to compare against
    base = Entry.create(:url => "http://rubyreports.org")

    assert_valid base

    e = Entry.new(:url => "http://rubyreports.org")

    # entry has an identical url, so we expect it to not be valid
    assert(!e.valid?, "Should not save entry unless url is unique")
    assert(e.errors.invalid?(:url), "Expected an error for duplicate url")
  end
end

Running the tests again, you should see that they pass. If you're paranoid, go ahead and remove the validates_uniqueness_of call from your model, and watch the tests fail.

Why no fixtures?

In the User tests for Tasty, I showed how to use fixtures just because its inevitable that you'll encounter them while working with Rails. However, it turns out that they've got a few sticky spots, and a lot of times, you simply don't need them.

By just explicitly calling Entry.new and Entry.create, it's very clear what our tests are checking for, and it's also reflecting how the code will actually be used, rather than relying on the implementation details of fixtures.

Making ugly assertions pretty via test_helper.rb

At the very top of your test, you see that rails requires the test_helper file. This is used for providing functions to simplify your tests and make them cleaner. One of the most common ways of doing this is to create custom assertions.

For example, it would be nice to have an assert_not_valid to match the assert_valid call in our tests. This is very easy to write:

test/test_helper.rb
# other code omitted
# add more helper methods to be used by all tests here...

  def assert_not_valid(model,msg="record was valid but shouldn't be")
    assert_block(msg) { !model.valid? }
  end

# ...

Pages: 1, 2, 3, 4, 5

Next Pagearrow