advertisement

Print

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

This is using test/unit's assert_block method to display the given message when the block returns false, and otherwise, pass the test if the block returns true.



This means that our old assertion:

assert !e.valid?, "Should not save entry unless url is unique"

now would look like this:

assert_not_valid e, "Should not save entry unless url is unique"

Custom assertions can become arbitrarily complex, and help keep your tests easily readable and focused on the actual things you are trying to verify. Since they are shared between all your tests, you can begin to establish a very high level set of assertions if needed.

If you want to play with this a little more, you might want to create something like an assert_invalid_field method that would work as shown below, but I'll leave the implementation details to you.

  assert_invalid_field :url, :for => e

What About That Stuff in validate()?

You'd also need to test Entry's custom validation, as clearly no one should be creating entries mentioning Rube Goldberg. The reason I didn't give this special treatment in the discussion of testing validations is that you handle it exactly the same way:

  def test_validates_rube_goldberg_check
    e = Entry.new(:url => "google.com", :short_description => "Nothing")
    assert_valid e

    f = Entry.new(:url => "apple.com",
                  :short_description => "Has Rube Goldberg")

    assert_not_valid f, "Should not save entry" 
    assert f.errors.invalid?(:short_description),
           "Expected an error for Rube reference" 
  end

Testing domain logic

We've also got a few other things that need testing in our Entry model, namely the tagging convenience methods and the date formatting methods. You essentially want to use your tests to define how you expect these methods to be used, and how you expect them to behave. The result is a fairly clear form of organic, self-verifying documentation:

test/unit/entry_test.rb
  # ...

  def test_tagging
    # needs to be put in DB because we're dealing with associations
    e = Entry.create(:url => "http://smurfs.com")
    e.tag_as(:foo)

    assert e.tagged_as?(:foo)
    assert !e.tagged_as?(:bar)

    assert_equal [:foo], e.tag_names

    e.tag_as(:bar)
    assert_equal [:foo,:bar], e.tag_names
  end


  def test_date_format
    e = Entry.create(:url => "http://snakesonaplane.com")
    assert_match /\A\d{4}\.\d{2}\.\d{2}\Z/, e.created_date
    assert_match /\A\d{4}\.\d{2}\.\d{2}\Z/, e.updated_date
  end

  # ...

I'm hoping that the above is easy enough to read, even for beginners. It's worth noting that I use a pattern to match my date format rather than use a specific date. I trust Rails to populate the underlying fields correctly, I just want to make sure the format is as expected.

By now it should be fairly clear that you can rather easily cover the use cases for your model. Once you write the tests once, they will continue to protect you down the line. Units will save you tremendous effort during refactoring, as they can quickly lead you to the source of various problems, and to a concise set of use cases that are easy to read.

Take control with functional tests

While it's true that your data model is often going to be the heart of your application, they're not the only thing that needs testing. Since controllers are the vital link between your models and views, they're just as test-worthy as a model. Luckily, it's pretty easy to handle the most important cases.

Tasty already has two controller actions we can test, the index pages for both entries and tags. When we generated those controllers, Rails already generated functional tests.

These default tests are already successfully doing nothing, but let's change them to be a little more useful.

The following set of tests checks to make sure that we can simply hit the tags index, whether or not any tags exist in the database:

test/functional/tags_controller_test.rb
require File.dirname(__FILE__) + '/../test_helper'
require 'tags_controller'

# Re-raise errors caught by the controller.
class TagsController; def rescue_iction(e) raise e end; end

class TagsControllerTest < Test::Unit::TestCase
  def setup
    @controller = TagsController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new

    Tag.destroy_all
  end

  def test_index_non_empty_tags
    Tag.create(:name => "foo")
    Tag.create(:name => "bar")

    get :index
    assert_response :success

    # assigns() lets you access instance variables assigned by the controller.
    tagnames = assigns(:urls_for_tags).keys
    %w[foo bar].each { |n| assert tagnames.include?(n) }
  end

  def test_index_empty_tags
    get :index
    assert_response :success
  end
end

Pages: 1, 2, 3, 4, 5

Next Pagearrow