Testing in Rails: Part 10 - Assertions

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

I dreamed I was assertive, image copyright Celia Perez
© Celia Perez

At this point in the series, we have covered most aspects of unit testing in Ruby on Rails. We learned the basics both in Ruby and in Rails and discussed how to write meaningful tests. We set up fixtures to make working with sample data easier. We learned to write tests for ActiveRecord objects and their relationships, validations, additional attributes and callbacks.

The only thing we have not done is write tests for the custom methods in our Winery and Wine models (such as: location, find_newest, age, is_antique?). But I feel confident that you have the testing skills to make that an easy task.

To ensure that you have everything you need for writing unit tests on your own, in this section I want to expand on the assertions you can utilize for unit testing.

Basic Assertions So Far

We have discussed six of the most common assertions already.

  • assert(boolean, message)
  • assert_equal(expected, actual, message)
  • assert_raise(exception, message) { block… }
  • assert_nothing_raised(exception, message) { block… }
  • assert_nil(object, message)
  • assert_valid(activerecord_object)

More Basic Assertions

There are three more basic assertions that you may find useful.

  • assert_not_equal(expected, actual, message)
  • assert_not_nil(object, message)
  • flunk(message)

assert_not_equal and assert_not_nil should be self-explanatory. They are simply the opposites of assert_equal and assert_nil.

flunk can be very helpful. It always fails. (It is the same as assert(false, message).) Why would an assertion that always fails be useful? Because you can embed it in loops and conditional statements. It allows you to perform more complicated boolean testing to decide if a test should pass or fail.

Let’s imagine that we have an application that is managing the inventory in both a store and a warehouse. The rules of our application should prevent a customer from purchasing an item that is completely out of stock, and should prevent the total inventory from falling below 5.

def test_inventory
    cart = {1 => 1, 3 => 2, 4 => 1, 5 => 1}
    inventory = {1 => 3, 2 => 30, 3 => 7, 4 => 19, 5 => 0}
    warehouse = {1 => 100, 2 => 24, 3 => 56, 4 => 32, 5 => 3}
    cart.each do |product_id, qty|
      total_inventory = inventory[product_id] + warehouse[product_id]
      if total_inventory - qty <= 0
        flunk("Product #{product_id} is out of stock.")
      elsif total_inventory - qty < 5
        flunk("Total inventory too low for product #{product_id}")
      end
    end
  end

Could we write this another way? Absolutely, especially for such a simple example. Technically, you do not need flunk, but it can be useful among complicated conditions and make your tests easier to read.

Advanced Assertions

There is a second group of assertions that I have elected to call “advanced”. The reason: most beginner and intermediate developers will never need them. You can safely skim over these. Just know they are available if you ever need them. Do not worry if you forget them—the functionality they provide is also achievable through a combination of standard Ruby and the basic assertions.

  • assert_match(pattern, string, message)
  • assert_no_match(pattern, string, message)
  • assert_in_delta(expected_float, actual_float, delta, message)
  • assert_instance_of( klass, object, message )
  • assert_kind_of( klass, object, message )
  • assert_respond_to( object, symbol, message )
  • assert_throws(expected_symbol, message) { block… }
  • assert_nothing_thrown(message) { block… }

Here is a test method to illustrate how each of these assertions can be used.

def test_advanced_assertions

    # test a string against a regular expression
    assert_match(/^\d,\d{3},\d{3}$/, "1,000,000", "Should match this format.")
    assert_no_match(/\d{3},\d{2}$/, "1,000,000", "Should not match this format.")
   
    # test that the price is withing the delta of the expected price
    price = 110
    assert_in_delta(100.0, price, 20.0, "Price should be between 80.00 and 120.00")
   
    # test instances and class kinds
    chinon = Wine.new
    assert_instance_of( Wine, chinon, "chinon should be an instance of Wine" )
    assert_kind_of( Wine, chinon, "chinon should be a kind of Wine" )
   
    # note that a class, such as Wine, is a kind of Class
    assert_kind_of( Class, Wine, "Wine should be a kind of Class" )
   
    # test that an object responds to a method (aka "message")
    # note that instances only respond to instance methods,
    #   while classes only respond to class methods.
    assert_respond_to( chinon, :age, "No response to :age" )
    assert_respond_to( Wine, :find_newest, "No response to :find_newest" )
   
    # if you like to use catch and throw in your code
    assert_throws(:done, "Array should be empty") { throw :done if [].empty? }
    assert_nothing_thrown("Array should not be empty") { throw :done if [1,2,3].empty? }

  end

Rare Assertions

There are four more assertions. They are extremely rare and their behavior can be replicated using other assertions. You will never need them. But for the sake of completeness, I will include them anyway.

  • assert_same( expected, actual, message)
    same as: assert_equal
  • assert_not_same( expected, actual, message)
    same as: assert_not_equal
  • assert_operator( object1, operator, object2, message)
    same as: assert( object1.operator(object2) )
  • assert_send([receiver, symbol, arg1, arg2], message)
    same as: assert( receiver.message(arg1, arg2) )

Assertion PDF

I have put all the assertions, with usage examples, into a single summary (a PDF file) for easy reference. (Can you see that I am trying to take away all of your excuses for not writing unit tests?)

The PDF also includes a list of the default validation error messages. I find this list helpful for testing what type of error was set when a validation fails.

assert_equal("can't be blank", person.errors[:name])

Using the PDF summary as a guide, try and write the rest of the unit tests on your own.

UPDATE: The series continues in Part 11.

One Response to “Testing in Rails: Part 10 - Assertions”

  1. A Fresh Cup » Blog Archive » Double Shot #149 Says:

    [...] Testing in Rails: Part 10 - Assertions - More in the continuing testing series from Null is Love. [...]

Leave a Reply