Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stubbing a method call that accepts a block in rspec

I have a lib method like this:

  def search(*args, &blk)
    MyGem.search(*args, &blk)
  end

I have another method that invokes this search method as follows:

  def members_of(dept)
    result = {}
    search(:attributes => ["displayname", "employeeID"]) do |entry|
      result[entry.employeeid.first.to_i] = entry.displayname.first.to_s
    end
    result
  end

When I write rspec test for members_of, I'm trying to stub the search call. But I can't figure out how exactly to stub the block, because the block is actually using one of the local variables defined in the method. Any help would be appreciated. Thank you.

like image 949
venkatKA Avatar asked Oct 25 '25 17:10

venkatKA


1 Answers

If using rspec-mocks, you can stub method calls to MyGem.search with a partial test double.

A partial test double is an extension of a real object in a system that is instrumented with test-double like behaviour in the context of a test. This technique is very common in Ruby because we often see class objects acting as global namespaces for methods.

To make a partial double of MyGem and expect a call to its search class method:

expect(MyGem).to receive(:search)

Then you can add additional behavior on this double to check the call arguments, make it yield values, or raise an error:

# match arguments
expect(MyGem).to receive(:search).with(:attributes => ["displayname", "employeeID"])
# yield value
expect(MyGem).to receive(:search).and_yield(42)
# yield multiple values
expect(MyGem).to receive(:search).and_yield(42).and_yield(987)
# raise an error
expect(MyGem).to receive(:search).and_raise(ArgumentError, "bad request")
# all at once!
expect(MyGem).to receive(:search).with(:attributes => ["displayname", "employeeID"])
    .and_yield(42)
    .and_raise(ArgumentError, "bad request")

Based on your information (which seems incomplete: dept argument is never used...), here is a fully working example:

Entry = Struct.new(:employeeid, :displayname)

class MyGem
  def self.search(*args)
    yield Entry.new([1, 2, 3], ["one", "two", "three"])
  end
end

class Organization
  def search(*args, &blk)
    MyGem.search(*args, &blk)
  end

  def members_of(dept)
    result = {}
    search(:attributes => ["displayname", "employeeID"]) do |entry|
      result[entry.employeeid.first.to_i] = entry.displayname.first.to_s
    end
    result
  rescue
    []
  end
end

RSpec.describe Organization do
  describe "#members_of" do
    it "calls search" do
      organization = Organization.new
      expect(MyGem).to receive(:search).with(:attributes => ["displayname", "employeeID"])
          .and_yield(Entry.new([1337, 100, 2000], ["leet", "one hundred", "two thousand"]))
          .and_yield(Entry.new([123], ["one-two-three"]))
      members = organization.members_of("dept")
      expect(members).to match({
          1337 => "leet",
          123 => "one-two-three"})
    end
    it "returns empty array on search error" do
      organization = Organization.new
      expect(MyGem).to receive(:search).and_raise(ArgumentError)
      expect(organization.members_of("dept")).to eq([])
    end
  end
end

Also, when using partial doubles, you should enable the verify_partial_doubles config option to ensure you are not stubbing class methods that do not exist. Enable it with this snippet in your spec/spec_helper.rb file:

RSpec.configure do |config|
  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end
end
like image 138
cbliard Avatar answered Oct 27 '25 09:10

cbliard