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:
- Full Name is a public function that some other objects use.
- We make this function into a protected one and use this method to make it public for testing.
- 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.
- 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;:to_sym) public *private_methods.collect(&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: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