Sunday, August 29, 2010

Testing private methods in RSpec

Why testing private methods? Well, it's not really about private vs public, if you want really fine granular unit tests that always only test no more than a couple of lines of code, you will need to do partial mocking and test private methods.

I heard argument that testing private methods exposes too much implementation and thus makes later refactoring harder. My argument is that unit test is part of the implementation. Fine grained "real unit" tests is very easy to read and understand. They help clarify the intent of that couple of lines of code in your target class. If you change implementation code, it should be perfect normal if you also need to change that simple unit test. On the other hand if your tests are in a larger granularity, then in each test, either you test a lot of code or your use a lot of mocking. Either case, chances are whenever you change implementation, you would need to change even more test code.

Another common practice is to extract private methods to another class and make them public and test from there. To me, there are only a few valid reasons to introduce a new class (or in general, to design), being able to test private methods isn't one of them.

Alright, with excuses all said (your argument is welcome), here is how I test private methods in ruby with rspec. I defined a global method in a file called describe_internally.rb in my test folder

def describe_internally *args, &block
example = describe *args, &block
klass = args[0]
if klass.is_a? Class
saved_private_instance_methods = klass.private_instance_methods
example.before do
klass.class_eval { public *saved_private_instance_methods }
end
example.after do
klass.class_eval { private *saved_private_instance_methods }
end
end
end

then whenever I need to test private methods for a class (say Foo), I use "describe_internally Foo" instead of "describe Foo". If you prefer, you can organize these two types of tests in the same file as the below example (say Foo is a class with two methods-a public one: "kick" and a private one: "aim" which returns the target to be kicked)

describe Foo do
describe "kick" do
it "should kick at where aim is" do
foo = Foo.new
foo.should_receive(:aim).with(steve_jobs).and_return :a_place
foo.kick steve_jobs
steve_jobs.should be_kicked_at :a_place
end
end
end
describe_internally Foo do
describe "aim" do
it "should aim at where the butt is" do
Foo.new.aim(steve_jobs).should be steve_jobs.butt
end
end
end

(Although in this case, I would just put all specs under the describe_internally block, because the first spec also requires knowledge of the private method and from my experience there is seldom any problem caused by that)
If you need an even more fine control of the scope of where you want private methods exposed, Jay Fields wrote a blog long ago giving another approach to achieve it http://blog.jayfields.com/2007/11/ruby-testing-private-methods.html