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

Class variable state leaked/persisted between examples #3087

Closed
francktrouillez opened this issue May 15, 2024 · 3 comments
Closed

Class variable state leaked/persisted between examples #3087

francktrouillez opened this issue May 15, 2024 · 3 comments

Comments

@francktrouillez
Copy link

francktrouillez commented May 15, 2024

Note

Please let me know if this issue is not created in the correct rspec project

Subject of the issue

Using class variables inside tests can lead to inconsistent behavior. Indeed, modifying a class variable in one test modifies it for everyone. I would expect each example to be independent of each other, and I wouldn't expect adding/modifying/deleting another unrelated example to influence my current test.

From my understanding, we don't clear the memory for class variables whenever we start a new example. That's maybe intended, but I'd be curious to know why we decided to go into that direction.

Do you know if there's a way to actually clean this up after examples?

Thanks for your help!

Your environment

  • Ruby version: 3.3.1
  • rspec-core version: 3.13.0

Steps to reproduce

# frozen_string_literal: true

begin
  require 'bundler/inline'
rescue LoadError => e
  warn '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.13.0'
  gem 'rspec-core', '3.13.0'
end

puts "Ruby version is: #{RUBY_VERSION}"
require 'rspec/autorun'

class A
  class << self
    attr_reader :foo

    def bar
      @foo ||= 0
      @foo += 1
    end
  end
end

RSpec.describe 'A' do
  context 'when A.bar is called' do
    before { A.bar }

    it { expect(A.foo).to eq(1) }
  end

  context 'when A.bar is called again' do
    before { A.bar }

    it { expect(A.foo).to eq(1) }
  end
end

Expected behavior

I would expect both examples to pass (even the second one), as I believe it should be independent and isolated from the first one.

Actual behavior

The second example fails, as the class variable @foo is not reset between examples.

image

@pirj
Copy link
Member

pirj commented May 15, 2024

Detecting and revering changes on a vast number of class objects in the object space, global variables, constants etc between each example is hard if even possible, error-prone and would have an insane overhead.

If you need isolation, use https://github.com/rspec/rspec-support/blob/8cbf7f9aecbefacd74da16ec2e713358ff72925b/lib/rspec/support/spec/in_sub_process.rb
Check https://github.com/search?q=org%3Arspec%20in_sub_process&type=code for usage examples.

@pirj pirj closed this as not planned Won't fix, can't repro, duplicate, stale May 15, 2024
@francktrouillez
Copy link
Author

Thanks for the quick answer! I was indeed expecting this, but wanted to make sure, in case I missed something.

Thanks!

@JonRowe
Copy link
Member

JonRowe commented May 16, 2024

This is an example of global state and we can't make any atttempt to reset this for you but I can suggest some approaches to help test it:

You can add a "reset" to this global state and call it between each spec in a hook, we actually do this ourselves in places.

You could also change how this state is encapsulated and instead of using multiple class variables you have a normal instance of a class which you test, but then have a single "class instance" of this class in reality, you can see an example of this with our "RSpec::World".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants