The name Unit testing explains the most special and powerful characteristic of this type of testing: the granularity of verifications is taken to the class size, which is the smallest cohesive unit we can find in an object-oriented application.
Despite these good intentions, tests often cross the borders of the single class because the system under test is constituted by more than one object: the main one which methods are called on (to verify the results returned with assertions) and its collaborators.
This is expected as no object is an island: there are very few classes which work alone. While we can skip the middle man on the upper side by calling methods directly on the system under test, there's no trivial way to wire the internal method calls to predefined result.
However, it's very important to find a way for insulating a class from its collaborators: while a small percentage of functional tests (which exercise an object graph instead of a single unit) are acceptable and required in a test suite, the main concern of a test-infected developer should be to have the most fine-grained test suite of the country:
- tests are very coupled to the production code, in particular to the classes which they use directly. It's a good idea to limit this coupling to the unit under test as much as possible;
- the possible interactions with a single object are a few, but integrating collaborators raises the number of situations that can happen very quickly;
- the collaborators could be very slow (relying on a database or querying a web service) or not even worth testing because it is not our responsibility;
- finally, if a collaborator breaks, not only its tests will fail but also the tests of the classes which use it. The defect could then be hard to locate.
The verb substitute is appropriate: the only way to keep out collaborators from a unit test is to replace them with other objects. These objects are commonly known as Test Doubles and they are subclasses or alternative implementations respectively of the class or interface required by the SUT. This property allows them to be considered instanceof the original collaborator class/interface and to be kept as a field reference or to be passed as a method parameter, which are the only ways I can think of to recognize a collaborator.
Dependency Injection plays a key role in many unit tests: in the case of a field reference on a class, there is the need to replace this reference with the Test Double one, to hijack the method calls on the collaborator itself. Since field references are usually private, it is difficult to access them (requiring reflection) and violating a class encapsulation to simplify testing does not seem a good idea.
Thus, the natural way to provide Test Doubles for collaborators is Dependency Injection, being it implemented via the constructor or via setters. Instead of instantiating the production class, simply produce an object of its custom-made subclass and use it during the construction of the SUT. My SUTs usually have optional constructor parameters to allow passing in a null when the collaborator is not needed at all.
While entering the Test Doubles world, the average programmer hears many terms which describes substitutes of an object's collaborators. In crescent order of complexity, they are:
- Dummies: object which do not provide any interaction, but are placeholders used to satisfy compile-time dependencies and to avoid breaking the null checks: no method is called on them but they are likely to be required parameters of a SUT method or part of its constructor signature. To avoid the creation of dummies I prefer to keep all constructor parameters optional, trusting that the Factory which creates the object passes regular collaborators instead of null values.
- Stubs: objects where some methods have been overridden to return canned results. They may have more than one precalculated result available, depending on the method parameters combination, but these variables are known values once you reach the act part of the test method.
- Mock objects: also known as Test Spies, mock objects are used in a testing style different from what we have worked on since the first part of this series (behavior based testing). They will be the main argument of the next episode.
- Fakes: objects which have a working implementation, but much simpler than the real collaborator one. An in-memory ArrayObject which substitutes a database result Iterator is an example of a Fake.
For Stubs and Mocks the situation is different: there are plenty of frameworks for nearly every language which provide help in generating them, which take care of evaluating the subclass code and instantiating an object. Phpunit incorporates a small mocking framework, accessible through the method getMock() on the test case class. Remember that while the method is named getMock(), both Stubs and Mocks can be created via this Api. In this part we'll focus again on state verification and we'll use a Stub to improve the granularity of a test.
We are going to give a meaningful example of unit testing using a Stub. In this example, a GeolocationService takes a User object and fills its latitude and longitude fields using the location specified. GeolocationService requires an instance of a fictional GoogleMaps object to work, and since we all love Dependency Injection it is passed in its constructor.
Note that GoogleMaps can also be an interface or a base abstract class: there is no technical difference. Moreover, if it was a less important collaborator it can even be passed as a locate() parameter.
This is the test case:
class GeolocationServiceTest extends PHPUnit_Framework_TestCase { public function testProvidesLatitudeAndLongitudeForAnUser() { $coordinates = array('latitude' => '42N', 'longitude' => '12E'); $googleMapsMock = $this->getMock('GoogleMaps', array('getLatitudeAndLongitude')); $googleMapsMock->expects($this->any()) ->method('getLatitudeAndLongitude') ->will($this->returnValue($coordinates)); $service = new GeolocationService($googleMapsMock); $user = new User; $user->location = 'Rome'; $service->locate($user); $this->assertEquals('42N', $user->latitude); $this->assertEquals('12E', $user->longitude); } }and here is the full, running example.
Note that I have created a Stub only for the external service and not for the User class. The former is external, slow, and unpredictable, but the latter is simple, with little or none internal behavior, and there's a small chance it will break. Nothing behaves like a String more than a String, as Misko says. The test method now focuses on exercising the locate() method and not also the GoogleMaps class.
Finally, let's take a look at the getMock() Api:
object getMock($originalClassName, [array $methods, [array $arguments,
[string $mockClassName, [boolean $callOriginalConstructor,
[boolean $callOriginalClone, [boolean $callAutoload]]]]]])$originalClassName is the name of the class you want to create a Stub/Mock for. $methods is a numeric indexed array containing the names of the methods you want to subclass; if left empty, every method will be substituted. $arguments are arguments to pass to the constructor of the original class (almost never used), while $mockClassName is a custom name for the subclass created.
The last three arguments are boolean values used to determine if leaving the original constructor or clone method, or to allow autoloading of $originalClassName. They default to true. Often you want to set $callOriginalConstructor to false if its signature requires other collaborators to be passed in. All arguments but the first one are optional.
The mock produced, along with the original class methods, also has the expects() one available. For now, simply calling it with the argument $this->any() will do the job. This method returns a PHPUnit_Framework_MockObject_Builder_InvocationMocker instance; in short, an internal object that you can call method() and will() on to decide the method name to replace and its predefined behavior. The simplest possible behavior is $this->returnValue(...), but also $this->returnArgument($argumentNumber) is available, along with $this->returnCallback($callbackName); refer to the phpunit documentation for supplemental informations.
I hope this introduction to Stub objects has helped you grasping the essence of Unit testing in php. Feel free to ask clarifications in the comments. In the next part of this series, we will explore the possibilities of Mock objects and behavior based testing.
You may want to subscribe to the feed to remain updated on new posts in this series.
No comments:
Post a Comment