Anthony
Anthony

Reputation: 15967

RSpec test runs correctly only when run in sequence

I have an issue that seems to be specific to my test suite.

I have a module that holds some defaults in a constant like so:

module MyModule

  DEFAULTS = {
    pool: 15
  }
  def self.options
    @options ||= DEFAULTS
  end

  def self.options=(opts)
    @options = opts
  end

end

module MyModule
  class MyClass

    def options
      MyModule.options
    end

    def import_options(opts)
      MyModule.options = opts
    end

  end
end

I allow the program to boot with no options or a user can specify options. If no options are given, we use the defaults but if options are given we use that instead. An example test suite looks like this:

RSpec.describe MyModule::MyClass do
  context "with deafults" do
    let(:my) { MyModule::MyClass.new }
    it 'has a pool of 15' do
      expect(my.options[:pool]).to eq 15
    end
  end
  context "imported options" do
    let(:my) { MyModule::MyClass.new }
    it 'has optional pool size' do
      my.import_options(pool: 30)
      expect(my.options[:pool]).to eq 30
    end
  end
end

If those tests run in order, great, everything passes. If it runs in reverse (where the second test goes first), the first test gets a pool size of 30.

I don't have a 'real world' scenario where this would happen, the program boots once and that's it but I'd like to test for this accordingly. Any ideas?

Upvotes: 0

Views: 154

Answers (2)

bliof
bliof

Reputation: 2987

You could always use a before(:each)

before(:each) do
  MyModule.options = MyModule::DEFAULTS
end

Side note - maybe a class for the configuration.

Something like:

module MyModule
  class Configuration
    def initialize
      @foo = 'default'
      @bar = 'default'
      @baz = 'default'
    end

    def load_from_yaml(path)
      # :)
    end

    attr_accessor :foo, :bar, :baz
  end
end

And then you could add something like this:

module MyModule
  class << self
    attr_accessor :configuration
  end

  # MyModule.configure do |config|
  #   config.baz = 123
  # end
  def self.configure
    self.configuration ||= Configuration.new
    yield(configuration)
  end
end

And finally you will reset the configuration in a more meaningful manner

before(:each) do
  MyModule.configuration = MyModule::Configuration.new
end

Upvotes: 0

Philip Hallstrom
Philip Hallstrom

Reputation: 19879

@options is a class variable in that module. I'm not sure that is technically the right name for it, but that's how it is acting. As an experiment, print out @options.object_id right before you access it in self.options. Then run your tests. You'll see that in both cases it prints out the same id. This is why when your tests are flipped you get 30. @options is already defined so @options ||= DEFAULTS is not setting @options to DEFAULTS.

$ cat foo.rb
module MyModule

  DEFAULTS = {
    pool: 15
  }
  def self.options
    puts "options_id: #{@options.object_id}"
    @options ||= DEFAULTS
  end

  def self.options=(opts)
    @options = opts
  end
end

module MyModule
  class MyClass

    def options
      MyModule.options
    end

    def import_options(opts)
      MyModule.options = opts
    end

  end
end

puts "pool 30"
my = MyModule::MyClass.new
my.import_options(pool: 30)
my.options[:pool]

puts
puts "defaults"
my = MyModule::MyClass.new
my.options[:pool]

And running it...

$ ruby foo.rb
pool 30
options_id: 70260665635400

defaults
options_id: 70260665635400

Upvotes: 1

Related Questions