Skip to content

testing protected and private methods in ruby

When I was looking for how to test protected an private methods in ruby on the net, I found many sites arguing whether you should, and several methods for doing so. I am of the opinion that if your method contains any logic at all, it should have a test. Some examples of what I consider logic:

Object:

class User
  validates_presence_of :first_name, :last_name, :company_id
  belongs_to :company
  def full_name
    "#{last_name}, #{first_name}"
  end
  def company_name
    company.name
  end
end

Test:

class UserTest < Test::Unit
  def test_full_name
    assert_equal("Gaffney, Mike", User.new(:first_name => "Mike", :last_name => "Gaffney"))
  end
  def test_company_name
    company = Company.new(:name => "CompanyName")
    user = User.new(:company => company)
    assert_equal("CompanyName", user.company_name)
  end
end

This may seem like overkill but when many people are looking at the code it greatly helps communicate intention to others.

Testing:

I also found several methods to test protected and private methods in ruby, so I will cover the pluses and minuses the ones I know of.

Lets say we have a User class and an unassociated comments class. Comments are only attributed to a poster’s full name. There is no direct ID link between comments and users.

class User
  validates_presence_of :first_name, :last_name
  def find_comments
    Comments.find(:all, :conditions => {:poster => full_name})
  end
  protected
  def full_name
    "#{last_name}, #{first_name}"
  end
end

Here are the approaches I’m going to use to test this:

  • Using a Mock to open access restrictions
  • Make single (protected/private) methods public on the tested class.
  • Make all (protected/private) methods public on the tested class.
  • Hybrid Mock Approach
  • Using instance_eval and send
  • send

Using Mocks:
Mocks should be pretty familiar to most developers. Basically you create a class or subclass that the test will use. In our case we are simply opening the access restrictions with a subclass. Our test class looks like this:

class MockUser < User
  def full_name
    super
  end
end
class UserTest < Test::Unit
  def test_full_name
    user = User.new(:first_name => "Mike", :last_name => "Gaffney")
    assert_equal("Gaffney, Mike" user.full_name)
  end
end

Plusses:

  • Simple and familiar to most developers from many languages

Minuses:

  • Can’t test private methods (subclass doesn’t have access).
  • With complicated classes you end up with quite a few mocks for all of the different cases. Managing the mocks becomes tedious.

Make single methods public:
Next we will make use of the runtime nature of ruby to change a function from protected to public. This also works for private methods:

class UserTest < Test::Unit
  def test_full_name
    User.send(:public, :first_name)
    user = User.new(:first_name => "Mike", :last_name => "Gaffney")
    assert_equal("Gaffney, Mike" user.full_name)
  end
end

or

class User
    public :first_name
end
class UserTest < Test::Unit
  def test_full_name
    user = User.new(:first_name => "Mike", :last_name => "Gaffney")
    assert_equal("Gaffney, Mike" user.full_name)
  end
end

Plusses:

  • Lets us directly access the private or protected function.

Minuses:

  • This pollutes the default namespace for all of the other tests. Remember that tests can (and should be able to) run in random order. Take for example:
    1. Full Name is a public function that some other objects use.
    2. We make this function into a protected one and use this method to make it public for testing.
    3. If this new test runs before the tests for the objects using the old public full_name, full_name will still be public even though it is actually protected.
    4. The other tests do not fail but the code will fail during runtime.

Make all methods public:
To save us some time on a big class, we can make all private and protected methods public:

class User
  public *protected_methods.collect(&amp;amp;amp;:to_sym)
  public *private_methods.collect(&amp;amp;amp;:to_sym)
end
class UserTest 
  def test_full_name
    user = User.new(:first_name => "Mike", :last_name => "Gaffney")
    assert_equal("Gaffney, Mike" user.full_name)
  end
end

or

class UserTest < Test::Unit
  def test_full_name
    User.send(:public, *MyClass.protected_instance_methods)  
    User.send(:public, *MyClass.private_instance_methods)
    user = User.new(:first_name => "Mike", :last_name => "Gaffney")
    assert_equal("Gaffney, Mike" user.full_name)
  end
end

This has the same issues as above but can be done one to make large classes easy to test, especially using the first method.

Hybrid Approach:
This works like the mock example and the previous example combined:

class AllAccessUser
  public *protected_methods.collect(&amp;amp;amp:to_sym)
end
class UserTest < Test::Unit
  def test_full_name
    user = AllAccessUser.new(:first_name => "Mike", :last_name => "Gaffney")
    assert_equal("Gaffney, Mike" user.full_name)
  end
end

This is easy like the previous but does not pollute the namespace. However it cannot be used for private methods.

Using send
Now we will use the built in reflection method to call the protected/private method on the class. This makes use of the fact that protected and private in ruby aren’t really like protected and private in other languages.

class UserTest < Test::Unit
  def test_full_name
    user = User.new(:first_name => "Mike", :last_name => "Gaffney")
    assert_equal("Gaffney, Mike" user.send(:full_name))
  end
end

Plusses:

  • Doesn’t pollute the namespace.
  • All code is right in the test.
  • Works for protected and private.

Minuses:

  • Some people don’t like using send

Summary:
Testing protected and private methods should be done, and ruby makes it much easier than some other languages. My preferred technique is the final one of using send. It is slightly less readable but keeps everything contained in one location.

14 Comments