0
votes

According to the rspec expect message docs, when we use code like:

expect(double).to receive(:message).with(argument)

and argument responds to #===, Rspec will call argument.=== on whatever is actually passed in. But what should I do if I want to check that a range of times is actually passed in?

For example, I wanted to test that this method sends the right message with the correct arguments (it's simplified):

def in_date_range(range)
  activities.by_created_at(range)
end

And a test like:

it 'passes correct arguments' do
  range = Time.new(0)..(Time.new(0) + 1.hour)

  expect(activities).to receive(:by_created_at).with(range)

  in_date_range(range)
end

This fails with TypeError: can't iterate from Time

I am suspecting it's because which rspec tries to call #=== on my time range and runs into trouble.

Is there a way to check that my range of times is actually the range I pass in?

I'm not actually looking for the times to overlap, but rather that the ranges are the same: That have the same start and end points.

I suppose one way we can do this is to write a custom matcher, but it seems an unusual route.

EDIT

Another possible solution is to split the method into two pieces:

def compute_range(time_in)
  (computations)
  time1..time2
end

def in_date_range(time_in)
  range = compute_range(time_in)
  activities.by_created_at(range)
end

Then I can test compute method that it returns the proper range, and the for the larger method I can test that passes in the correct input, and takes the output of the smaller method it takes the result from the first and passes into the other method, sidestepping the confusion of checking ranges in message.

This doesn't fully answer the question - how do you check 'receive a specific range' with the rspec receive(:message), but is enough for me right now.

3

3 Answers

1
votes

You can, of course, just expect(..).to receive(:foo) { |arg| do_something } and check your expectation in the do_something block. The following should work:

range = Time.new(0)..(Time.new(0) + 1.hour)

expect(activities).to receive(:by_created_at) { |arg| 
                        expect(arg.first).to eq(Time.new(0))
                      }

This works because a failed expects raises a rspec specific exception, and the RSpec runner doesn't care where in the execution such an exception would be raised, as long as it is during execution of the it block.

0
votes

From makandracards:

Test if two date ranges overlap in Ruby or Rails

A check if two date or time ranges A and B overlap needs to cover a lot of cases:

A partially overlaps B A surrounds B B surrounds A A occurs entirely after B B occurs entirely after A One trick to cover all these cases with a single expression is to see if the start date of each range is looking at the end date of the other range in the same direction.

The code below shows how to implement this in Ruby on Rails. The example is a class Interval, which has two attributes #start_date and #end_date. These dates are considered inclusive, meaning that if one interval ends on the day another interval starts, we consider those two intervals overlapping.

Note how we implemented both a version for loaded Ruby objects (Interval#overlaps?) and a scope that returns all overlapping intervals (Interval.overlapping). Depending on your problem you might need one or both or those.

class Interval < ActiveRecord::Base

  validates_presence_of :start_date, :end_date

  # Check if a given interval overlaps this interval    
  def overlaps?(other)
    (start_date - other.end_date) * (other.start_date - end_date) >= 0
  end

  # Return a scope for all interval overlapping the given interval, including the given interval itself
  named_scope :overlapping, lambda { |interval| {
    :conditions => ["id <> ? AND (TIMEDIFF(start_date, ?) * TIMEDIFF(?, end_date)) >= 0", interval.id, interval.end_date, interval.start_date]
  }}

end

Explanation of the algorithm

You can picture the algorithm like this: "If the beginnings of both ranges look at the end of the other range, do they both look in the same direction?"

If you draw on a piece of paper you can see that this statement is true for all cases where both ranges overlap:

A partially overlaps B A surrounds B B surrounds A You can also see that the statement is false where the ranges do not overlap:

A occurs entirely after B B occurs entirely after A

0
votes

EDIT:

I've had a bit more time to look at this, and I think the error lies somewhere within the activities.by_created_at(range). This must be trying to call a loop on the range.

Below are passing specs. I've create a dummy Thing class and run tests against it, and everything passes:

class Thing
  def initialize( name )
    @name = name
  end

  def in_date_range(range)
    activities.by_created_at(range)
  end
end

describe Thing do
  specify 'Thing test' do
    name = 'Tim'
    expect(Thing).to receive(:new).with( name )

    Thing.new(name)
  end

  specify 'Time test' do
    range = Time.new(0)..(Time.new(0) + 1.hour)
    t = Thing.new('Jimmy')

    expect(t).to receive(:in_date_range).with( range )

    t.in_date_range( range )
  end
end

~/test (master)
18:22:21$ rspec spec/models/thing_spec.rb 
Loading Enhanced Logger.
Run options: include {:focus=>true}
..

Finished in 0.14094 seconds (files took 2.89 seconds to load)
2 examples, 0 failures

END EDIT

The problem is not with rspec checking the range.

I ran this and it worked:

describe Thing do
  specify 'Thing test', :focus do
    range = 1..4
    expect(Thing).to receive(:new).with( range )

    Thing.new(1..4)
  end
end


Run options: include {:focus=>true}
.

Finished in 0.09132 seconds (files took 2.59 seconds to load)
1 example, 0 failures

The problem is with making Time iterable.

See this question: Iterate over Ruby Time object with delta

The second part of the accepted answer should help you.