Mocking a classmethod, such as Time.now, that is also called by
the testing infrastructure (eg within the context of mocha_verify
or add_failure), can lead to meltdown. The attached test,
abstracted from a large rails application, ends up in an infinite
recursion.
Versions of mocha up to 0.9.0 avoided this problem. A change in
the logic of Mocha::Mock.method_missing is responsible for the
changed behavior. Modifying method_missing to work like this would
restore the old behavior:
def method_missing(symbol, *arguments, &block)
if @responder and not @responder.respond_to?(symbol)
raise NoMethodError, "undefined method `#{symbol}' for #{self.mocha_inspect} which responds like #{@responder.mocha_inspect}"
end
if matching_expectation_allowing_invocation = @expectations.match_allowing_invocation(symbol, *arguments)
matching_expectation_allowing_invocation.invoke(&block)
else
matching_expectation = @expectations.match(symbol, *arguments)
if matching_expectation
return matching_expectation.invoke # the old behavior
elsif !@everything_stubbed
message = UnexpectedInvocation.new(self, symbol, *arguments).to_s
message << Mockery.instance.mocha_inspect
raise ExpectationError.new(message, caller)
end
end
end
This is probably not a good way to resolve this issue, it's just
to illustrate what changed. With the old behavior, when a mocked
classmethod like Time.now is called by mocha after the test method
has completed, the mocked value is returned. The current behavior
is that the mocked value is only returned if a new invocation is
allowed, otherwise an exception is raised (and caught) and we can
end up in an infinite recursion.
I think a cleaner solution lies in the direction of unstubbing
everything immediately after the test method has returned, before
calling mocha_verify. Similarly each of the rescue clauses (eg for
Mocha::ExpectationError) should also unstub before calling
add_failure or add_error. This way mocha would then be using the
original classmethod, instead of (inappropriately) continuing to
use the mocked version after running the test.