Testing protected and private methods (Intermediate)
A common question of those that are getting started with unit testing is, how are protected and private methods tested? Protected and private methods are not uncommon and the desire to test the code in them should be natural. The confusion that arises from how to test these methods is created at least in part by the thought that they must be tested independently.
In the book Pragmatic Unit Testing, Dave Thomas and Andy Hunt had this to say:
In general, you don't want to break any encapsulation for the sake of testing (or as mom used to say, "don't expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods. If there is significant functionality that is hidden behind private or protected access, that might be a warning sign that there's another class in there struggling to get out.
Using the public interface of your class is by far the best way to test protected and private methods. If you find yourself unable to do this, PHPUnit and PHP itself offer solutions to test these methods directly.
How to do it...
The following code in the CardCollection
class is used to add a card to the collection:
<?php class CardCollection implements IteratorAggregate { // ... public function addCard(Card $card) { array_push($this->cards, $card); } // ... }
The following test can be used to ensure the object state is modified accordingly:
<?php class CardCollectionTest extends PHPUnit_Framework_TestCase { // ... public function testAddCardAffectAttribute() { $card = new Card('A', 'Spades'); $this->cardCollection->addCard($card); $this->assertAttributeEquals(array($card), 'cards', $this->cardCollection); } // ... }
How it works...
This test shows how you can inspect the private or protected state of a given object. PHPUnit has a series of attribute assertions that you can use to test the value of any attribute on a class even if it has protected or private visibility. Whenever possible you should use the public interface of an object to test this; however, in the event that it is not possible, the attribute assertions can come in very handy. The assertAttributeEquals()
method is similar to its non-attribute counterpart assertEquals()
. However, instead of passing the value you are testing, you pass the name of the attribute you want to test as the second parameter and the object that attribute is set on as the third parameter. As always, the expected value is passed in as the first parameter.
PHPUnit contains attribute equivalents for the standard set of assertions. You can compare values, check contents of arrays, compare array counts, and so on. Anything you would typically do with a variable in a unit test can also be accomplished in attributes using the attribute assertions.
Private and protected methods
PHPUnit doesn't provide the same functionality above for private and protected methods. However, if you are using PHP 5.3.2 or higher you can use reflection to alter the visibility of the method you are trying to test.
In CliFormatter
there is a private method, getCard(),
that is used to format a given card into a readable string.
<?php class CliFormatter { // ... private function getCard(Card $card) { return $card->getNumber() . substr($card->getSuit(), 0, 1); } // ... }
Using reflection we can expose this method and invoke it as a part of a test.
<?php class CliFormatterTest extends PHPUnit_Framework_TestCase { // ... public function testGetCard() { $method = new ReflectionMethod('CliFormatter', 'getCard'); $method->setAccessible(true); $card = new Card('A', 'Spades'); $this->assertEquals('AS', $method->invoke($this->formatter, $card)); } // ... }
The ReflectionMethod::setAccessible()
method can be used to allow a method to be invoked. However, you must invoke that method using the ReflectionMethod::invoke()
method. If we attempted to call $this|formatter|getCard()
directly then it would fail. This does keep us from having to clean up the accessibility. Your client code will continue to work as you originally wrote it. You don't have to worry about the method continuing to be accessible.