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
Post a Comment