Martynas
Martynas

Reputation: 2565

Ruby. Mocking in RSpec

I have a problem with mocking. I have class DistanceMatrix and I would like to indicate which method form_matrix was called in if/else statement. I need to use mocha and RSpec. Any ideas?

class DistanceMatrix

 def initialize(*args)
    if args[0].class == String
      form_matrix(get_data_from_yaml(args[0], args[1]))
    elsif args[0].class == Array || args[0] == nil
      form_matrix(get_data_from_db(args[0]))
    end
 end

 def form_matrix(...)
  ...
 end

end

it tried:

describe DistanceMatrix, "when mocking ..." do
  it "should do call form_matrix" do
    DistanceMatrix.any_instance.expects(:form_matrix).with([1]).once
    DistanceMatrix.any_instance.expects(:get_data_from_yaml).with("file_name.yml").once.returns([1])
    DistanceMatrix.new("file_name.yml")
  end
end

but got error:

Failures:
  1) DistanceMatrix when mocking ... should do call form_matrix
     Failure/Error: DistanceMatrix.new("file_name.yml")
     unexpected invocation: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml', nil)
     unsatisfied expectations:
     - expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml')
     - expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.form_matrix([1])
     satisfied expectations:
     - allowed any number of times, already invoked once: #<DistanceMatrix:0x9e48b40>.get_optimal_route(any_parameters)
     - allowed any number of times, already invoked once: #<Database::Distances:0x9d59798>.load_distances(any_parameters)
     # ./distance_matrix.rb:18:in `initialize'
     # ./tsp_algorithm_spec.rb:253:in `new'
     # ./tsp_algorithm_spec.rb:253:in `block (2 levels) in <top (required)>'
Finished in 0.25979 seconds

I found that in RSpec we should use not .expects() but .should_receive(), so I tried:

describe DistanceMatrix, "when mocking ..." do
  it "should do call form_matrix" do
    DistanceMatrix.any_instance.should_receive(:form_matrix).with([1])
    DistanceMatrix.any_instance.should_receive(:get_data_from_yaml).with("file_name.yml").and_return([1])
    DistanceMatrix.new("file_name.yml")
  end
end

but got new failure:

Failures:
  1) DistanceMatrix when mocking ... should do call form_matrix
     Failure/Error: DistanceMatrix.any_instance.should_receive(:form_matrix).with([1])
     (#<Mocha::ClassMethods::AnyInstance:0x96356b0>).form_matrix([1])
         expected: 1 time
         received: 0 times
     # ./tsp_algorithm_spec.rb:251:in `block (2 levels) in <top (required)>'

Finished in 0.26741 seconds

Upvotes: 1

Views: 7042

Answers (2)

James Mead
James Mead

Reputation: 3502

I only have experience with using Mocha and not RSpec, but looking at the Mocha failure message, the key parts are these :-

unexpected invocation: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml', nil)
unsatisfied expectations:
- expected exactly once, not yet invoked: #<AnyInstance:DistanceMatrix>.get_data_from_yaml('file_name.yml')

If you look at the ends of these lines, you will notice that get_data_from_yaml is not being called with the expected parameters. It is being called with ('filename.yml', nil) and not ('filename.yml') as expected.

This is happening because when you call DistanceMatrix.new("file_name.yml") in your test with only one argument and then inside DistanceMatrix#initialize DistanceMatrix#get_data_from_yaml is being called with (args[0], args[1]) and since args is a single element array, args[1] will be nil.

Maybe this isn't how you expected Ruby to work, but the following demonstrates this behaviour :-

def foo(*args)
  puts "args[0]=#{args[0].inspect}; args[1]=#{args[1].inspect}"
end

foo("string") # => args[0]="string"; args[1]=nil

Upvotes: 4

Keith Gaddis
Keith Gaddis

Reputation: 4113

DistanceMatrix.any_instance.expects(:form_matrix).with("String") # => supply the correct string param

or

DistanceMatrix.any_instance.expects(:form_matrix).with([]) # => supply the correct array param

I'm not sure what your get_data_from_db and get_data_from_yaml methods are doing, but you should be able to control those inputs as well to verify the correct arguments are being supplied to form_matrix.

EDITED You'll have to use DistanceMatrix.any_instance instead of mocking on an instance variable because you're trying to mock something in the initializer. Also, in case its unclear, you'll need to actually make the appropriate method call after you set up the mock in the lines above, e.g.

DistanceMatrix.new("SomeString")

EDITED

it "should do call #form_matrix with proper arguments" do
  DistanceMatrix.any_instance.expects(:form_matrix).with([1])
  DistanceMatrix.any_instance.expects(:get_data_from_yaml).with("foo").returns([1])
  DistanceMatrix.new("foo")
end

Upvotes: 0

Related Questions