53

Given a class with a couple of instance variables and some methods. Some instance variables are set accessible via attr_reader and attr_accessor. Thus the others are private.
Some of the private instance variables get set within one of the instance methods and read/utilized within another method.

For testing I'm using RSpec. As I'm still new to Ruby and want to get all things right, I defined my tests being rather fine-grained. Thus I've got one describe block for each instance method, which themselves are partitioned into a subset of contexts and its. General environmental preconditions are defined with before.

However, when testing one of the methods, which is utilizing but not setting one of the private variables, I need to call the other method, which is setting this variable. This seems rather overweight and not modular for me.

Is there a way of forcing a private instance variable to a certain value. Similar to "ripping out" the value of a private instance variable with Object::instance_eval(:var).

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
Torbjörn
  • 5,512
  • 7
  • 46
  • 73

3 Answers3

98

As you answered in your question the easiest way to set instance variable is with instance_eval method:

obj.instance_eval('@a = 1')

Another way is to use instance_variable_set:

obj.instance_variable_set(:@a, 1)

But I would not recommend to do this in your specs. Specs are all about testing behavior of an object and testing behaviour by breaking class encapsulation with instance_eval will make your tests more fragile and implementation dependent.

Alternative approach to object state isolation is to stub accessor methods:

class UnderTest
  attr_accessor :a

  def test_this
    do_something if a == 1
  end
end

#in your test
under_test = UnderTest.new
under_test.stub(:a).and_return(1)
Aliaksei Kliuchnikau
  • 13,589
  • 4
  • 59
  • 72
  • Haven't thought about stubbing accessors yet. Would probably make a lot of my testing data (some text files, which should be detected and parsed by my program) obsolete. Seems a nice way. Thanks :) – Torbjörn Jan 27 '12 at 19:18
  • Just to clarify, in your answer, are you claiming that `instance_eval` breaks class encapsulation or that `instance_variable_set` does as well? – Seanny123 Dec 16 '13 at 08:28
  • @Seanny123 Both `instance_eval` and `instance_variable_set` modify internal state that is private to the instance and should not be modified outside of the object. – Aliaksei Kliuchnikau Dec 16 '13 at 10:25
  • 1
    Isn't it also bad to create an accessor for the sole purpose of testing, since it gives the next person that reads your code the impression that the object's state will modified by an external object in the normal running of the program? – Seanny123 Dec 16 '13 at 10:49
  • @Seanny123 Usually when you find yourself wanting to stub private variable then it is a sign that you need some kind of dependency injection (be it `attr_accessor`, constructor parameter or something else) and thus this state is part of the interface (meaning you just want to protect its modification by "mutator" method). Also sometimes it might be a smell in the design (but this is a broad topic and a little bit out of scope of this question). – Aliaksei Kliuchnikau Dec 16 '13 at 10:59
  • There are cases where you would want to stub a private variable and can't use attr_accessor, e.g., `def current_user @current_user || User.find(session[:user_id]) end` – konyak Feb 18 '14 at 17:01
  • @ChaseT. This is why we have `attr_accessor`s - sometimes they are transformed into methods like this and this does not break anything in our system. And technique for stubs remains the same: `under_test.stub(:current_user => user_obj1)` – Aliaksei Kliuchnikau Feb 18 '14 at 21:27
12

Use instance_variable_set:

class SomeClass
  attr_reader :hello
  def initialize
    @hello = 5
  end
  # ...
end

a = SomeClass.new
a.hello    # => 5

a.instance_variable_set("@hello", 7)
a.hello    # => 7
robbrit
  • 17,560
  • 4
  • 48
  • 68
1

I just solved it by creating a child and adding an accessor:

class HasSecrets
  @muahaha
end

class TestMe < HasSecrets
  attr_accessor(:muahaha)
end

def test_stuff
  pwned = TestMe.new()
  pwned.muahaha = 'calc.exe'
end
Chris
  • 5,788
  • 4
  • 29
  • 40