Open Thanks

You can find entire code example in this gits

RSpec has some really nifty built in matchers. In this article we will have a look at be_within matcher.

The syntax is pretty simple

value          = 10.0001
expected_value = 10.0001
delta          =  0.0001

expect(value).to be_within(delta).of(expected_value)

The most common example where to use be_within is when comparing floating point values if their are in a range/delta of acceptance (e.g. if you returning float value from database, Math calculations, …)

require 'rspec'

class Foo
  def float_range_example
    33 * 5 * Math::PI
  end
end

RSpec.describe(Foo) do
  subject { described_class.new }

  describe '#float_range_example' do
    it do
      expect(subject.float_range_example).to be_within(0.01).of(518.3627878423158)
    end
  end
end

But these matchers can be also used to compare delta of Time values.

require 'rspec'
require 'time'

class Foo
  # ...

  def time_range
    Time.now
  end
end

RSpec.describe(Foo) do
  # ...

  describe '#time_range' do
    context 'in pure Ruby' do
      it do
        expect(subject.time_range).to be_within(2).of(Time.now)
      end
    end
  end
end

If you’re using ActiveSupport (part of a Ruby on Rails framework) you may want to pass 2.secconds instead for better readability:

require 'rspec'
require 'active_support/time'

RSpec.describe(Foo) do
  # ...

  describe '#time_range' do
    # ...

    context 'in Rails' do
      it do
        expect(subject.time_range).to be_within(2.seconds).of(Time.now)
      end
    end
  end
end

Delta value (range of acceptance) is really up to you to decide but I had once a situation where my laptop had SSD drive and coworkers laptop had HDD. When he run his test suite some of the tests were failing just because of too tight delta value on Time.now

Many of you may be already familiar with the fact that RSpec 3 match matcher can compare similarities in a Hash. Did you know that you can pass other matchers to it, including regular expressions and be_within matchers?

class Foo
  # ...

  def hash_with_range_value
    {
      id: 1,
      title: 'SPS J.Murgasa',
      city: "Banska Bystrica",
      created_at: Time.now
    }
  end
end

RSpec.describe(Foo) do
  # ...

  describe '#hash_with_range_value' do
    it do
      expect(subject.hash_with_range_value).to match({
        id: 1,
        title: 'SPS J.Murgasa',
        city: /[bB]an/,
        created_at: be_within(1).of(Time.now)
      })
    end
  end
end

If this test would fail you would get helpful output similar to this:

Failures:

  1) Foo#hash_with_range_value should match {:id=>1, :created_at=>(be
within 1 of 2016-06-29 19:48:21 +0100), :city=>/[bB]an/}
     Failure/Error: expect(subject.hash_with_range_value).to match({
       expected {:id=>1, :created_at=>2016-06-29 19:38:21.681774449
+0100, :city=>"Banska Bystrica"} to match {:id=>1, :created_at=>(be
within 1 of 2016-06-29 19:48:21 +0100), :city=>/[bB]an/}
       Diff:
       @@ -1,4 +1,4 @@
       -:city => /[bB]an/,
       -:created_at => (be within 1 of 2016-06-29 19:48:21 +0100),
       +:city => "Banska Bystrica",
       +:created_at => 2016-06-29 19:38:21.681774449 +0100,
        :id => 1,

Pretty readable !

If you comparing values in Array you can do:

class Foo
  # ...

  def array_with_range_values
    [Time.now, Time.now.midnight, Time.now.midday, 3.days.ago]
  end
end

RSpec.describe(Foo) do
  # ...

  describe '#array_with_range_values' do
    it do
      expect(subject.array_with_range_values).to match_array([
        be_within(2.seconds).of(Time.now),
        be_within(2.seconds).of(Time.now.midnight),
        be_within(2.seconds).of(Time.now.midday),
        be_within(2.seconds).of(Time.now - 3.days)
      ])
    end
  end
end

Resouces used in this article: