Best way to test Ruby class methods?

by john on September 7, 2011

Let’s say that we have a Ruby object with some class methods. In my case, we want to be able to inject a driver at the class level that all instances will use:


class Mod::Thingy
  def self.driver
  end
  def self.driver=(d)
  end
end

And let’s say that .driver has a default value. So the class looks more like this:


class Mod::Thingy
  def self.driver
    @driver || DefaultDriver
  end
  def self.driver=(d)
    @driver = d
  end
end

Now here’s the dealio. Let’s say that we test for the default value first. So we check Mod::Thingy.driver == DefaultDriver. Then we test the setter. We do Mod::Thingy.driver = OtherDriver, and then check the return value of Mod::Thingy.driver. Fair enough.

But what if we ran the examples in reverse? Now we do Mod::Thingy.driver = OtherDriver first, check it; but now in our second test, we can’t test the default value, because the state of Mod::Thingy has been changed.

Strategies:

  1. We might use RSpec’s around feature to save the state of Mod::Thingy.driver and restore it afterwards. What I don’t like about this is that now the around method has to know all about all of the setters at the class level. Meanwhile, it is possible to imagine objects that are difficult to set back to their default state: Perhaps the class methods have order dependencies themselves. This would make the test need to know a lot about the inner state of the object.
  2. We might provide a .reset! method on the object. But this seems a bit nutty: Add a method just to make testing easier?
  3. What about this in an RSpec before(:each) block:

    
        Mod.send(:remove_const, 'Thingy')
        load "#{PROJECT_ROOT}/lib/mod/thingy.rb"
    

    Now we’re getting a nice clean class. To me, (3) is more parallel to the testing convention of new’ing up an object instance for the test.

Thoughts?

{ 2 comments… read them below or add one }

Jeremy Weiskotten September 20, 2011 at 6:50 pm

I may not be grokking all the details here, but I’d lean away from using a class method to define the driver. Instead, I’d use constructor dependency injection, passing an instance of the desired driver to the constructor, with a sensible default.

class Mod::Thingy
attr_reader :driver

def initialize(driver=DefaultDriver.new)
@driver = driver
end
end

Then inject an instance of OtherDriver (or a mock) in the tests. This also happens to be thread-safe — no race conditions, so no synchronization needed.

admin September 20, 2011 at 7:29 pm

After reviewing a lot of options, the best strategy seems to be:

(a) inject the driver via a class method. Why? Because then you can do Mod::Thingy.new and get it (and all Mod::Thingys) instantiated with the “right” driver.

(b) Run an after handler to reset the class.

It would take awhile for me to outline the reasons, but briefly: Driver injection at the “instance” level is definitely an option, but doesn’t fit very well with the idea that you should be able to do Mod::Thingy.new whenever you want. Injection into an instance implies a factory, which would be fine in Java but seems like overkill in Ruby . . .

Leave a Comment

Previous post: Spying on Ruby’s Net::HTTP

Next post: US Airways Mastercard error message