zuhao
zuhao

Reputation: 129

How to test only one of the multiple method calls in RSpec?

In a method I have multiple calls to another method with different arguments. I only want to test one particular call to see if the arguments for that call match some condition. Is there a better way to do so than stubbing every other call?

For example, I have

def some_method
  foo(1)
  foo('a')
  foo(bar) if ... # some complex logic
  foo(:x)
  ...
end

I just want to test if foo is actually called with argument bar.

subject.should_receive(:foo).with(correct_value_of_bar)

But what to do with other calls of foo inside the same some_method?

Upvotes: 1

Views: 592

Answers (1)

Zach Dennis
Zach Dennis

Reputation: 1784

Okay, so based on your latest comment you want to observe that you're logging some output. Here's an example where you observe this behavior by replacing STDOUT with a StringIO instance:

# foo.rb
require 'rubygems'
require 'rspec'
require 'rspec/autorun'

require 'stringio'

class Foo
  def something
    puts 1
    puts 'a'
    puts 'bar' if true # some complex logic
    puts :x
  end
end


describe Foo do
  describe '#something' do
    context "and something complex happens" do
      let(:io){ StringIO.new }
      it "logs to STDOUT" do
        $stdout = io
        Foo.new.something
        expect(io.tap(&:rewind).read).to include("bar\n")
      end
    end
  end
end

This will work, but doing this has side-effects that reach far beyond your specific example because we're changing the global $stdout. This can be improved by using poor man's dependency injection with constructor defaults:

class Foo
  def initialize(io=STDOUT)
    @io = io
  end

  def something
    puts 1
    puts 'a'
    puts 'bar' if true # some complex logic
    puts :x
  end

  protected

  def puts(*args)
    @io.puts *args
  end
end


describe Foo do
  describe '#something' do
    context "and something complex happens" do
      let(:io){ StringIO.new }
      it "logs to STDOUT" do
        Foo.new(io).something
        expect(io.tap(&:rewind).read).to include("bar\n")
      end
    end
  end
end

In the above example we give ourselves the ability to pass in the IO object that we'll be putting to. This lets us observe the behavior without having side-effects beyond the scope of the test and in a way that lets the object we're testing stay true to itself (ie: we're not modifying the object itself as with the previous but now deleted comment about using as_null_object suggested).

You could also use an options hash on your constructor and push the lazy assignment into initialize itself:

def initialize(arg1, arg2, options={})
   @io = options[:io] || STDOUT
end 

And you could also upgrade your simple puts to use an actual Logger object. Then you could test in one place that your logger worked with STDOUT, STDERR or where ever and you could test in all of the objects where you cared about logging that it was logging to info, debug, etc appropriately.

You could take this in a few more directions as well, but without knowing more about what you're doing this potential answer is likely already long enough.

Hopefully this gives you some ideas as to how you could approach this by observing the behavior rather than relying on internal implementation details (like the fact the you're using puts itself as opposed to print "bar\n" or another method which outputs text to an IO object.

Upvotes: 1

Related Questions