Prevent mocks leaking out of their scope when mocked more than once in different nested scopes.
Created by: expeehaa
Subject of the issue
When mocking a method in an outer and inner scope using combinations of expect
and allow
, the mocks leak out of the inner scope. I suppose that this is a bug because it only happens when the method was first mocked in the outer scope.
If it is a feature it is a pretty unintuitive one.
Your environment
- Ruby version: 2.6.6, 3.0.1
- rspec-mocks version: 3.10.2
Steps to reproduce
#!/usr/bin/env ruby
# frozen_string_literal: true
begin
require 'bundler/inline'
rescue LoadError => e
$stderr.puts 'Bundler version 1.10 or later is required. Please update your Bundler'
raise e
end
gemfile(true) do
source 'https://rubygems.org'
gem 'rspec', '~> 3.10.0'
gem 'rspec-mocks', '3.10.2'
end
puts "Ruby version is: #{RUBY_VERSION}"
require 'rspec/autorun'
RSpec.describe 'nested space' do
# Succeeds.
it 'does not leak without previous mocks' do
String.new
RSpec::Mocks.with_temporary_scope do
allow(String).to receive(:new).once do
allow(String).to receive(:new).and_raise 'fail'
end
String.new
end
expect{String.new}.not_to raise_error
end
context 'when mocking with allow in the outer scope' do
context 'when mocking with allow in the inner scope' do
# Fails.
it 'does not leak return values' do
allow(String).to receive(:new).once
String.new
RSpec::Mocks.with_temporary_scope do
allow(String).to receive(:new).once do
allow(String).to receive(:new).and_raise 'fail'
end
String.new
end
expect{String.new}.not_to raise_error
end
end
context 'when mocking with expect in the inner scope' do
# Fails.
it 'does not leak counting' do
allow(String).to receive(:new).once
String.new
RSpec::Mocks.with_temporary_scope do
expect(String).to receive(:new).once.and_return('test')
String.new
end
expect(String.new).not_to eq 'test'
end
end
end
context 'when mocking with expect in an outer scope' do
context 'when mocking with allow in the inner scope' do
# Fails.
it 'does not leak return values' do
expect(String).to receive(:new).once
String.new
RSpec::Mocks.with_temporary_scope do
allow(String).to receive(:new).once do
allow(String).to receive(:new).and_raise 'fail'
end
String.new
end
expect{String.new}.not_to raise_error
# Even if the line above did not fail, the test would fail because String.new was called 3 times instead of just once.
pending
end
# Fails.
it 'does not leak counting' do
expect(String).to receive(:new).twice
String.new
RSpec::Mocks.with_temporary_scope do
allow(String).to receive(:new).twice
String.new
String.new
end
String.new
end
end
end
end
Expected behavior
All tests should pass.
Actual behavior
Failures:
1) nested space when mocking with allow in the outer scope when mocking with allow in the inner scope does not leak return values
Failure/Error: expect{String.new}.not_to raise_error
expected no Exception, got #<RuntimeError: fail> with backtrace:
# test_spec.rb:53:in `block (5 levels) in <main>'
# test_spec.rb:53:in `block (4 levels) in <main>'
# test_spec.rb:53:in `block (4 levels) in <main>'
2) nested space when mocking with allow in the outer scope when mocking with expect in the inner scope does not leak counting
Failure/Error: expect(String.new).not_to eq 'test'
(String (class)).new(no args)
expected: 1 time with any arguments
received: 2 times
# test_spec.rb:70:in `block (4 levels) in <main>'
3) nested space when mocking with expect in an outer scope when mocking with allow in the inner scope does not leak return values
Failure/Error: expect{String.new}.not_to raise_error
expected no Exception, got #<RuntimeError: fail> with backtrace:
# test_spec.rb:91:in `block (5 levels) in <main>'
# test_spec.rb:91:in `block (4 levels) in <main>'
# test_spec.rb:91:in `block (4 levels) in <main>'
4) nested space when mocking with expect in an outer scope when mocking with allow in the inner scope does not leak counting
Failure/Error: String.new
(String (class)).new(no args)
expected: 2 times with any arguments
received: 3 times
# test_spec.rb:109:in `block (4 levels) in <main>'
Finished in 0.01682 seconds (files took 0.05268 seconds to load)
5 examples, 4 failures
Failed examples:
rspec test_spec.rb:40 # nested space when mocking with allow in the outer scope when mocking with allow in the inner scope does not leak return values
rspec test_spec.rb:59 # nested space when mocking with allow in the outer scope when mocking with expect in the inner scope does not leak counting
rspec test_spec.rb:78 # nested space when mocking with expect in an outer scope when mocking with allow in the inner scope does not leak return values
rspec test_spec.rb:97 # nested space when mocking with expect in an outer scope when mocking with allow in the inner scope does not leak counting