ruby - How to set a negative message expectation with a verifying double in RSpec? -


i test conditional call obj.my_method in following code:

def method_to_test(obj)   # ...   obj.my_method if obj.respond_to?(:my_method)   # ... end 

obj can either foo or bar. 1 implements my_method, other doesn't:

class foo   def my_method; end end  class bar end 

my test structured this:

describe '#method_to_test'   context 'when obj foo'     let(:obj) { instance_double('foo') }     'calls my_method'       expect(obj).to receive(:my_method)       method_to_test(obj)     end   end    context 'when obj bar'     let(:obj) { instance_double('bar') }     'does not call my_method'       expect(obj).not_to receive(:my_method)  # <- raises error       method_to_test(obj)     end   end end 

the first example passes second raises error when setting negative message expectation because obj verifying double not implement my_method:

#method_to_test   when obj foo     calls my_method   when obj bar     not call my_method (failed - 1)  failures:    1) #method_to_test when obj bar not call my_method      failure/error: expect(obj).not_to receive(:my_method)        bar class not implement instance method: my_method 

i understand why rspec raises error. how can test line despite obj being verifying double?

ps: i've enabled verify_partial_doubles, changing instance_double('bar') object_double(bar.new) or bar.new doesn't help.

interesting , tricky question! rspec docs, found this:

[edit: saw understand why rspec complains, can jump possible dirty / less dirty solutions parts. i'll keep explanations found here in case else face similar issue (and please correct me / add further info on that!)]

verifying instance doubles not support methods class reports not exist since actual instance of class required verify against. commonly case when method_missing used. - https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles/dynamic-classes

the docs cite issues instance_double , usage of method_missing, believe it's same issue you're facing. bar class not report respond :my_method. in fact, in failing test, method_to_test(obj) never gets called, test fails when try define expectation on instance_double:

describe '#method_to_test'   context 'when obj bar'     let(:obj) { instance_double('bar') }      'does not call my_method'       puts "a"       expect(obj).not_to receive(:my_method)  # <- raises error       puts "b"  # <- never runs       method_to_test(obj)     end   end end 

produces:

#method_to_test   when obj bar     not call my_method (failed - 1) 

when try adding expectation, rspec looks list of methods bar class reports have defined, , not find :my_method, , fails preventively, thinking it's helping identify issue in tests (after you're trying add method call expectation on not exist, it's typo, right? - ouch!).

possible dirty solution

so, i'm afraid there no consistent way add expectation instance_double you're using without dirty trickery. example define :my_method on bar able add message expectation, have redefine respond_to? in context of bar, resulting in this:

def my_method   raise # should never executed in case end  def respond_to?(m_name)   m_name != :my_method && super(name) end 

but why want that? dirty , counter-intuitive! type of strategy can work on specific case we're talking about, couple tests lot specific implementation , void value documentation.

possible less dirty solutions

so, suggestion not rely on method expectations, on desired behavior. want execution of method_to_test(obj) not raise exception because of method missing on bar instance. can accomplish way:

describe '#method_to_test'   context 'when obj bar'     let(:obj) { instance_double('bar') }      'does not call my_method'       expect { method_to_test(obj) }.not_to raise_error     end   end end 

the downside if exception raised when executing method body, test fail, while, ideally, want fail only when nomethoderror raised. may try this, won't work:

    'does not call my_method'       expect { method_to_test(obj) }.not_to raise_error(nomethoderror)     end 

the reason error raised in case :my_method called on obj not of type nomethoderror of type rspec::mocks::mockexpectationerror:

#method_to_test   when obj bar     not call my_method (failed - 1)  failures:    1) #method_to_test when obj bar not call my_method      failure/error: expect { method_to_test(obj) }.not_to raise_error        expected no exception, got #<rspec::mocks::mockexpectationerror: #<instancedouble(bar) (anonymous)> received unexpected message :my_method (no args)> backtrace: 

you use rspec::mocks::mockexpectationerror instead of nomethoderror expectation, create fragile tests: if change test bar instance real obj instead of instance_double, raise nomethoderror if :my_method called? test keep passing while not testing worth @ all. (as pointed out in comments), if this, rspec warn you:

"warning: using expect { }.not_to raise_error(specificerrorclass) risks false positives, since literally other error cause expectation pass, including raised ruby (e.g. nomethoderror, nameerror , argumenterror), meaning code intending test may not reached. instead consider using expect {}.not_to raise_error. message can supressed setting: rspec::expectations.configuration.warn_about_potential_false_positives = false."

so, between these options, i'd not specify type of exception.

possible (best) solution

finally, i'd suggest refactoring. using respond_to? indication you're lacking proper interface defined , implemented across these multiple classes. can't on specifics of refactoring highly dependent of current code context / implementation details. should evaluate if worth cost-wise, , if not i'd suggest extracting obj.my_method if obj.respond_to?(:my_method) dedicated method call, asserting not raise_error more reliable assertion, given you'd have less context strange exceptions raised. also, if this, can pass simple double object allow add message expectations way intended to.

sorry --verbose answer :d


Comments

Popular posts from this blog

sql - VB.NET Operand type clash: date is incompatible with int error -

SVG stroke-linecap doesn't work for circles in Firefox? -

python - TypeError: Scalar value for argument 'color' is not numeric in openCV -