Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude stubbed classes from subclasses after teardown #1570

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions lib/rspec/mocks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,11 @@ class << self

# To speed up boot time a bit, delay loading optional or rarely
# used features until their first use.
autoload :AnyInstance, "rspec/mocks/any_instance"
autoload :ExpectChain, "rspec/mocks/message_chain"
autoload :StubChain, "rspec/mocks/message_chain"
autoload :MarshalExtension, "rspec/mocks/marshal_extension"
pirj marked this conversation as resolved.
Show resolved Hide resolved
autoload :AnyInstance, "rspec/mocks/any_instance"
autoload :ExpectChain, "rspec/mocks/message_chain"
autoload :StubChain, "rspec/mocks/message_chain"
autoload :MarshalExtension, "rspec/mocks/marshal_extension"
autoload :ExcludeStubbedClassesFromSubclasses, "rspec/mocks/exclude_stubbed_classes_from_subclasses"

# Namespace for mock-related matchers.
module Matchers
Expand Down
17 changes: 17 additions & 0 deletions lib/rspec/mocks/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def initialize
@verify_partial_doubles = false
@temporarily_suppress_partial_double_verification = false
@color = false
@exclude_stubbed_classes_from_subclasses = false
end

# Sets whether RSpec will warn, ignore, or fail a test when
Expand Down Expand Up @@ -178,6 +179,22 @@ def color?
end
end

def exclude_stubbed_classes_from_subclasses?
@exclude_stubbed_classes_from_subclasses
end

# When this is set to true, stubbed classes are excluded from the list of
# subclasses of the parent class after each spec.
def exclude_stubbed_classes_from_subclasses=(val)
@exclude_stubbed_classes_from_subclasses = !!val

if val
RSpec::Mocks::ExcludeStubbedClassesFromSubclasses.enable!
else
RSpec::Mocks::ExcludeStubbedClassesFromSubclasses.disable!
end
end

# Monkey-patch `Marshal.dump` to enable dumping of mocked or stubbed
# objects. By default this will not work since RSpec mocks works by
# adding singleton methods that cannot be serialized. This patch removes
Expand Down
50 changes: 50 additions & 0 deletions lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took RSpec::Mocks::MarshalExtension for example.

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
module RSpec
module Mocks
# Support for `exclude_stubbed_classes_from_subclasses` configuration.
#
# @private
class ExcludeStubbedClassesFromSubclasses
def self.enable!
return if Class.respond_to?(:subclasses_with_rspec_mocks)

require 'weakref'

mod_something = Module.new do
pirj marked this conversation as resolved.
Show resolved Hide resolved
def excluded_subclasses
@excluded_subclasses ||= []
@excluded_subclasses.select(&:weakref_alive?).map do |ref|
ref.__getobj__
rescue RefError
nil
end.compact
end

def exclude_subclass(constant)
@excluded_subclasses ||= []
@excluded_subclasses << WeakRef.new(constant)
end
end
RSpec::Mocks.extend(mod_something)
pirj marked this conversation as resolved.
Show resolved Hide resolved
pirj marked this conversation as resolved.
Show resolved Hide resolved

Class.class_eval do
def subclasses_with_rspec_mocks
subclasses_without_rspec_mocks - RSpec::Mocks.excluded_subclasses
end

alias subclasses_without_rspec_mocks subclasses
alias subclasses subclasses_with_rspec_mocks
end
end

def self.disable!
return unless Class.respond_to?(:subclasses_with_rspec_mocks)

Class.class_eval do
undef subclasses_with_rspec_mocks
alias subclasses subclasses_without_rspec_mocks
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rubocop triggers this warning :

lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb:47:11: W: Lint/DuplicateMethods: Method RSpec::Mocks::ExcludeStubbedClassesFromSubclasses::Class#subclasses is defined at both lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb:38 and lib/rspec/mocks/exclude_stubbed_classes_from_subclasses.rb:47.
          alias subclasses subclasses_without_rspec_mocks
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It's wrong because the method is undefined at line 46. I disabled this warning.

undef subclasses_without_rspec_mocks
end
end
end
end
end
2 changes: 2 additions & 0 deletions lib/rspec/mocks/mutate_const.rb
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ def to_constant
end

def reset
RSpec::Mocks.exclude_subclass(@mutated_value) if RSpec::Mocks.configuration.exclude_stubbed_classes_from_subclasses?
@constants_to_transfer.each do |const|
@mutated_value.__send__(:remove_const, const)
end
Expand Down Expand Up @@ -297,6 +298,7 @@ def to_constant
end

def reset
RSpec::Mocks.exclude_subclass(@mutated_value) if RSpec::Mocks.configuration.exclude_stubbed_classes_from_subclasses?
@parent.__send__(:remove_const, @const_name)
end

Expand Down
66 changes: 66 additions & 0 deletions spec/rspec/mocks/exclude_stubbed_classes_from_subclasses_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
class TestClass
end

module RSpec
module Mocks
RSpec.describe ExcludeStubbedClassesFromSubclasses do
after do
described_class.disable!
end

describe '.enable!' do
it 'does not extends Class when it has been enabled' do
described_class.enable!

expect {
described_class.enable!
}.not_to(change { Class.respond_to?(:subclasses_with_rspec_mocks) })
end

it 'extends RSpec::Mocks with methods' do
described_class.enable!
expect(RSpec::Mocks).to respond_to(:excluded_subclasses)
end

it 'extends Class with methods' do
expect {
described_class.enable!
}.to change { Class.respond_to?(:subclasses_with_rspec_mocks) }.from(false).to(true)
end

it 'excludes stubbed classes from subclasses' do
::RSpec::Mocks.space.reset_all
RSpec::Mocks.configuration.exclude_stubbed_classes_from_subclasses = true

subclass = Class.new(TestClass)
stub_const('TestSubClass', subclass)

::RSpec::Mocks.space.reset_all
expect(TestClass.subclasses.map(&:object_id)).not_to include(subclass.object_id)
end
end

describe '.disable!' do
it 'does nothing when it has not been enabled' do
expect { described_class.disable! }.not_to raise_error
end

it 'removes methods when it has been enabled' do
described_class.enable!
expect {
described_class.disable!
}.to change { Class.respond_to?(:subclasses_with_rspec_mocks) }.from(true).to(false)
end

it 'does not exclude stubbed classes from subclasses' do
subclass = Class.new(TestClass)

stub_const('TestSubClass', subclass)

::RSpec::Mocks.space.reset_all
expect(TestClass.subclasses).to include(subclass)
end
end
end
end
end
30 changes: 30 additions & 0 deletions spec/rspec/mocks/mutate_const_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,21 @@ def change_const_value_to(value)
it 'returns nil' do
expect(hide_const(const_name)).to be_nil
end

describe 'with global exclude_stubbed_classes_from_subclasses option set' do
include_context "with isolated configuration"

before do
RSpec::Mocks.configuration.exclude_stubbed_classes_from_subclasses = true
end

it 'gives the same subclasses after rspec clears its mocks' do
original_subclasses = TestClass.subclasses
stub_const(const_name, Class.new(TestClass))
reset_rspec_mocks
expect(TestClass.subclasses).to eq(original_subclasses)
end
end
end

describe "#hide_const" do
Expand Down Expand Up @@ -351,6 +366,21 @@ def change_const_value_to(value)
end
end
end

describe 'with global exclude_stubbed_classes_from_subclasses option set' do
include_context "with isolated configuration"

before do
RSpec::Mocks.configuration.exclude_stubbed_classes_from_subclasses = true
end

it 'gives the same subclasses after rspec clears its mocks' do
original_subclasses = TestClass::Nested.subclasses
stub_const('TestClass', Class.new(TestClass::Nested))
reset_rspec_mocks
expect(TestClass::Nested.subclasses).to eq(original_subclasses)
end
end
end

context 'for a loaded nested constant' do
Expand Down
Loading