marauder7 marauder7 - 2 months ago 11
Ruby Question

RSpec testing of a class which uses a gem object as an instance variable

So I'm pretty new to Rspec and I'm trying to figure out how to write tests for a class that takes an object as a constructor parameter and sets that object to an instance variable. Then it calls that instance variable's object methods in other methods.

Example:

class ClassA
def initialize(string_object, gem_object)
@instance_variable1 = gem_object
@string = string_object
end

def check_validity?(some_arg)
unless @instance_variable1.gemObjectMethod1.gemObjectMethod2(some_arg).empty?
return true
end
false
end
..
..
end


I feel very lost in how to write specifications for this. For one I don't really understand what specifying a constructor actually entails. What I realize is that I'd have to find some way of mocking or stubbing the gem_object I'm getting as argument, but I'm not sure how.

For the next method, what I've tried to this point is:

describe '#check_validity?' do
context 'gets empty list' do
let (:actual) { subject.check_validity?("sample") }
before do
allow(subject).to receive(@instance_variable1.gemObjectMethod1.gemObjectMethod2).with("sample").and_return([])
end
it 'returns false' do
expect(actual).to be false
end
end
end


But this gives me error relating to my constructor saying that it expected 2 arguments but was given 0.

Any help would be much appreciated! Also, I couldn't really find anything on line about specifying constructors with their arguments mocked. Maybe I'm looking in the wrong place or maybe missing something obvious as this is my first experience with BDD.

Answer

In RSpec, 'receive' is a method that accepts a symbol that represents the name of a method. (It allows you to chain a 'with' method that accepts the expected list of parameters.) To fix the before-block you could do this:

before do
  allow(subject.instance_variable_get(:@instance_variable1).gemObjectMethod1).to receive(:gemObjectMethod2).with("sample").and_return([])
end

The sheer ugliness of that, though, suggests that something is wrong. And it is. The code is violating the law of demeter pretty badly and the test is being drawn into it.

As a first attempt to clean it up, you might consider a method that "caches" the results of calling @instance_variable1.gemObjectMethod1. Let's say that that first method returns an enumerable group of widgets. You could change your class to include something like this:

def check_validity(a_string)
  widgets.gemObjectMethod2(a_string).empty?
end

def widgets
  @widgets ||= @instance_variable1.gemObjectMethod1
end

Your class still knows a bit too much about the gem object, but now you have broken it down in such a way that you could refactor how you find widgets -- perhaps a different gem or your own implementation of it. For the purposes of your testing, you can isolate that decision from the test by mocking widgets.

let(:gem_widgets) do
  instance_double(GemObjectMethod1ResultClass, gemObjectMethod2: true)
end

before do
  allow(subject).to receive(:widgets).and_return(gem_widgets)
  allow(gem_widgets).to receive(:gemObjectMethod2).with("sample").
    and_return([])
end

it 'should pass with "sample"' do
  expect(actual).to eql true
end
Comments