When a test case calls methods that write new records to a database, sometimes the test needs to fetch those records back and inspect them. This post develops assert_latest, an assertion that detects newly created records.

The following methods use Ruby on Rails, and its exquisite persistence layer, ActiveRecord. The techniques apply to developer tests on any platform.

We start with a trivial method that accepts an Array of prop names, and creates a prop record for each one:

  def create_props(props)
    props.each{|u|  Prop.create!(:name => u)  }
  end

And here’s an incomplete test case that doesn’t know if the method worked:

  def test_might_create_props
    create_props(%w[hip hop dont stop])
  end

When we design code for testing, our code should not have too many extra lines. If only test cases need a line of code, we should work to take it out.

Sometimes Ruby makes design-for-testing very easy. If we change .each to .map, our tests get more bang for very little buck, and our fictitious method gets a better interface:

  def return_created_props(props)
    return props.map{|u|  Prop.create!(:name => u)  }
  end

  def test_return_created_props
    names = %w[hip hop dont stop]
    props = return_created_props(names)
    assert_equal names, props.map(&:name)
      #  more assertions here
  end

Sometimes code that creates new database records grows more complex than our simple example. Specifically, when a Rails test case simulates a Controller responding to a web page, any new data records might not come back in the assigns() system.

We need an assertion that detects new items, to write our test like this:

  def test_find_created_props
    names = %w[hip hop dont stop]
    
    props = assert_latest Prop do
      create_props(names)
    end
    
    assert_equal names, props.map(&:name)
  end

That decouples the test from the tested code. Here’s how it works.

assert_latest calls get_latest, which inspects the record’s maximum id key before its block, and returns all the records created after the block.

  def assert_latest(model, message = nil, &block)
    return get_latest(model, &block) ||
                flunk_latest(model, message, true)
  end

If the method returns nil, we flunk the assertion.

Every assert_ needs a deny_; we just reverse the polarity:

  def deny_latest(model, message = nil, &block)
    get_latest(model, &block) and
        flunk_latest(model, message, false)
  end

That fails when your production code accidentally creates new records that it should not.

get_latest fetches the maximum id, if any. Then it calls your block, allowing it to attempt to write records.

  def get_latest(model, &block)
    max_id = model.maximum(:id) || 0
    block.call
    all = model.find( :all,
                      :conditions => "id > #{max_id}",
                      :order => "id asc" )
    return *all # <-- returns nil for [], 
                #     one object for [x], 
                #     or an array with more than one item
  end

After your block returns, we find all records with an id greater than our “high water mark”. This technique depends on databases that increment their id keys monotonically and eternally; if your database is configured to perform some other way, you’ll need a better technique.

The little star * converts the array into a kind of “inline expression”, as if all = [1,2,3] were instead all = 1,2,3. That trick permits us to return nil, one new record, or an array of records.

And, finally, any assertion failure should provide complete diagnostics, to help rapidly determine how to revert or debug the code:

  def flunk_latest(model, message, polarity)
    flunk build_message( message, 
                         "new #{model.name.pluralize} should " +
                         "#{'not' unless polarity} be created" )
  end

The best persistence layers operate as records in databases when you want them to, and as objects in memory when you don’t. assert_latest decouples your tests from your production code, using ActiveRecord’s powerful techniques that decouple your objects from your database.