Convenient Trick for Tolerance-based Test Assertions Using #ish

Sometimes it’s hard to just say what you mean.

When writing automated system and integration tests, I occasionally find it tricky to express the precise expected value of test output. For example, I expect the timestamp on a database record to be within a second or two of “now,” but I can’t predict the exact millisecond it will be created by the web server I’m posting to. And sometimes a floating point result is off by a millionth or trillionth, because the numeric representations used in my tests are not necessarily the binary equivalent of what the code under test is using.

In the cases where I know the differences are circumstantial (and irrelevant to my test), I find myself wanting to write…


assert_equal record.created_at, Time.now.ish
assert_equal bullet.velocity, 5.25.ish

…and have it “just work.” I know what I mean, and so (likely) will anyone reading the test after me.

I know, I know… most test tools already provide some sort of tolerance-based assertion helpers or value matchers. (MiniTest has #assert_in_delta and RSpec has #be_close.) But they can damage readability and obscure intent, and sometimes they simply don’t fit the situation.

In the following example, I want to compare a Hash of attribute values for my space ship after ticking the game engine one quantum into the future:


@game_clock.tick
@space_ship.attrs.should == { x: 120, y: 0, rotation: 0 }

This looks simple enough, but if I happen to be employing a physics simulator, my results may be off by a very minuscule amount, and I won’t be able to predict that difference. Worse, I can’t employ #be_close because it won’t know what I mean by feeding it a Hash. Better:


@space_ship.attrs.should == { x: 120.ish, y: 0.ish, rotation: 0.ish }

Here’s the code that enables #ish for Numeric values:


class Numeric
  def ish(acceptable_delta=0.001)
    ApproximateValue.new self, acceptable_delta
  end
end
class ApproximateValue
  def initialize(me, acceptable_delta)
    @me = me
    @acceptable_delta = acceptable_delta
  end

  def ==(other)
    (other - @me).abs < @acceptable_delta
  end

  def to_s
    "within #{@acceptable_delta} of #{@me}"
  end
end

Here's how you can do something similar with Time:


class Time
  def ish
    ApproxTime.new(self)
  end
end
class ApproxTime
  def initialize(t); @time = t; end
  def ==(o)
    return false if @time.nil? or o.nil?
    (@time - o).abs < 5
  end
end

Note that it's easy to provide for optional tolerance tuning (eg, to be a little looser, you could do something like 3.14.ish(0.01)).

I generally call this function #ish because it's short, unlikely to collide with any other method names, and it's fun to say "five-ish" than "assert in delta five comma oh-point-oh-one. It also implies a semantic dependence on the object we're invoking the #ish method on. It's easy to deduce that 9.ish could mean something quite different from (3.days.from_now).ish.

I tend to implement the #ish helper on a per-project basis, in order to localize my assumptions for reasonable default tolerances. Is plus-or-minus 5 seconds a reasonable tolerance for all people on all projects? No. But for any given project, it's usually easy to just pick something and move on.

If monkey-patching Ruby's built-in value types gives you heartburn, you might instead implement a helper function #about or roughly that accepts any object and determines, internally, how best to inject tolerance into equality tests.

Anyway, I just wanted to share another of our sneaky little testing tricks. Anybody interested in seeing this become a Rubygem? Better yet, have you got any of your own tricks you'd like to share with us?
 

Conversation
  • […] Trick for Tolerance-based Test Assertions Using #ish […]

  • Comments are closed.