Prevent mocks leaking out of their scope when mocked more than once in different nested scopes.
Open
Nobody has claimed this yet.
- Dominant language
- Ruby
- Stars
- 117
- Forks
- 51
- Avg merge
- 59m
- Merged PRs (30d)
- 3
Description
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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the provided reproduction and trace RSpec::Mocks.with_temporary_scope together with the nested allow and expect calls. Add regression coverage for all four failing examples, then verify that inner return values and call counts do not affect the outer scope and that all tests pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ruby
- Domain
- testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100