11

I'm testing a class level instance variable (and setters) in a gem using RSpec. I need to test the following:

  1. The correct default value is provided if the setter is never used.
  2. The variable can be successfully updated through the setters.

Obviously there is a run order issue here. If I change the values using the setters, I lose memory of what the default value was. I can save it to a variable before the setter test and then reset the value at the end, but that only protects me if all setter tests follow the same practice.

What is the best way to test the default value of the variable?

Here is a simple example:

class Foo
  class << self
    attr_accessor :items
  end
  @items = %w(foo bar baz) # Set the default
  ...
end

describe Foo do

  it "should have a default" do
    Foo.items.should eq(%w(foo bar baz))
  end

  it "should allow items to be added" do
    Foo.items << "kittens"
    Foo.items.include?("kittens").should eq(true)
  end
end

3 Answers3

15
class Foo
  DEFAULT_ITEMS = %w(foo bar baz)

  class << self
    attr_accessor :items
  end

  @items = DEFAULT_ITEMS
end

describe Foo do
  before(:each) { Foo.class_variable_set :@items, Foo::DEFAULT_ITEMS }

  it "should have a default" do
    Foo.items.should eq(Foo::DEFAULT_ITEMS)
  end

  it "should allow items to be added" do
    Foo.items << "kittens"
    Foo.items.include?("kittens").should eq(true)
  end
end

Or maybe a better way is to reload the class

describe 'items' do
  before(:each) do
    Object.send(:remove_const, 'Foo')
    load 'foo.rb'
  end
end
David Hempy
  • 5,373
  • 2
  • 40
  • 68
Ismael Abreu
  • 16,443
  • 6
  • 61
  • 75
  • It's an @variable at the class level. If I use a `before(:each)`, the default could be changed in code and the tests wouldn't notice. –  Dec 28 '12 at 14:46
  • Good solution. I was tunnel visioned on making the test work for the code instead of making my code more testable. Thank you for the nudge back on track! :) –  Dec 28 '12 at 15:44
12

If your class has internal states that you would like to test I find that using the class_variable_get a nice way of approaching this. This does not require you to expose any of the variables in the class, so the class can stay untouched.

it 'increases number by one' do
    expect(YourClass.class_variable_get(:@@number)).to equal(0)
    YourClass.increase_by_one()
    expect(YourClass.class_variable_get(:@@number)).to equal(1)
end

I know this is not what you ask for in your question, but it is in the title, which got me here.

Automatico
  • 12,420
  • 9
  • 82
  • 110
  • Thanks for the addition. You are right that this is a bit different, but hopefully that will help others who find this post and are looking for something a bit different than what I was solving. –  Dec 11 '13 at 15:14
3

I found this question pursuing a slightly different problem -- clearing a cached class variable between rspec examples.

In a module, I have an expensive class config, which I cache like this:

module Thingamizer
  def config
    @config ||= compute_config_the_hard_way()
  end
end

class Thing
   extend Thingamizer
end

In my rspec tests of Thing, compute_config_the_hard_way was only called the first time. Subsequent calls used the cached version, even if I mock compute_config_the_hard_way to return different things in other tests.

I resolved this by clearing @config before each example:

 before { Thing.instance_variable_set(:@config, nil) }

Now the the thing I was hung up on is that @config is a class variable, not an instance variable. I tried many variations of class_variable_set without luck.

The wrinkle here is that Thing (the class) is actually an instance of Class. So what seems to be a class variable in a class method is actually an instance variable, in an instance of Class (i.e. Thing). Once I wrapped my head around that idea, using instance_variable_set instead of class_variable_set made perfect sense.

See Using Instance Variables in Class Methods - Ruby for a discussion of class variables as instance variables.

David Hempy
  • 5,373
  • 2
  • 40
  • 68