This series of articles is an experimental attempt to explain problems of unit tests in PHP applications, turning the theory upside down. From my practice I know it well that even skilled developers often have difficulties with rearranging their OOP knowledge onto new test driven approach. I will try to share my experience with the subject in form of original questions and answers, that elucidate very important and not always obvious aspects of unit testing and clean code practices.

In the first part we’ll consider the common misunderstandings of the OOP and unit testing principles, that may lead to wrong perception of test driven design practices.

Table of contents

My class has a lot of private methods. How to test them?

The short answer: 1) you don’t need to; 2) decompose the class to make private methods public, but in another class.

In unit tests only public methods should be tested.

Protected and private methods are backstage. They are part of classes design, but we don’t really test the design. We test what to expect from objects and how they communicate. And objects communicate only via public methods. Public methods define interfaces of classes. We test the communication based on interfaces.

No changes in protected or private methods should affect expectations from a class. The expectations are defined in unit tests. They may be documented and external applications may rely on them. So if expectations are changed, unit tests become invalid. If it’s about a library, backward-incompatibility is introduced. If this happens implicitly, it means your tests are coupled with the design and they are fragile. Fragile tests are expensive and inconvenient.

If a class encapsulates a lot of complex logic, interaction with other classes, you will probably quickly feel that it’s almost impossible to check all eventual outputs or behaviors of this class. Most likely it’s a clear case of single responsibility principle violation. Usually it may be solved by decomposition: splitting one class to several ones and making them collaborate. The private methods that encapsulate some logic and that you can’t help testing, they just become public in another class.

Decomposition to make private methods testable? But what about the encapsulation?

It is often taken wrong what public methods are and how far encapsulation can go. I’d say it happens as result of overdoing the “natural logic” in OOP design, thinking of it only on the top level. You might say I contradict the basics of the theory that states that OOP objects should represent natural objects. But it’s only an apparent paradox.

For instance let’s take an object represents a growing sunflower. The sunflower tilts during the day to face the sun. Let’s assume you have an input of a date and time and want to get the angles of flower’s head at that exact moment. It seems quite natural to ask the sunflower object about these angles, as well as about the average amount of water consumed depending on soil moisture and average insolation. But in fact the calculations behind these answers involve many other objects: the sun angles calculator, the water consumption calculator, the soil moisture geographical reference book and so on. You should not encapsulate all this in one object, even though it might seem natural for an external observer (see examples 1.1-1.2).

That’s why the publicity of methods is not only an interface for top levels of applications (e.g. for controllers). Objects interact on all levels and do different job, expected by specific collaborators. So even a very small low-level task should have an API, ideally defined by an interface and controlled by public methods. In Java you can vary the publicity on the level of classes, not only methods. That’s of course much more natural, but in PHP you can only keep it in mind.

Example 1.1:

Example 1.1. Sunflower object, UML Diagram

For instance you have an object represents a plane geometrical curved figure described by a function f(x). To test a method of calculation the area of the figure (geometrical interpretation of a definite Integral) you don’t really need to mock the object that supplies the integral calculus. For unit testing it would be much more natural just to input standard figures with beforehand known area and assert results. That’s enough. No real need of inversion.

Even more trivial example is when you instantiate data objects. They’re usually used for communication, so they’re often returned as result. No need to mock when you can check the result directly, no matter how it was created.

Unfortunately it’s a rather rare case. But people tend to see exactly this case before they realize it’s not the one, when they’re already caught in a trap. Thus the general rule I would recommend here is still to avoid encapsulated dependencies.

2. Dependencies are services

Services could be allocated somewhere in a global registry. For instance using the Service locator pattern. An example of such a service is in-memory caching: it is not a thing to be passed through layers of an application, one instance for all clients accessed via the service locator is enough.

Another (more often recommended) way of handling the problem is a Dependency injection container framework, like this Symfony component.

3. Dependencies are collaborators

In this (the most frequent case) dependencies usually have to be passed directly via constructors (if they’re coherent through the entire class) or even directly to methods as arguments. This approach makes you lift the instantiation new objects to top levels of an application, which may look scary. But the good news is that this principle really helps to diagnosticate bad design on early stages. If you start doing this, you will quickly feel there’s something like a magic drives you to design much better.

What we’re talking about is the Dependency Inversion principle, that basically states:
change classes to depend on abstractions, not on concretions.

So instead of instantiating a class inside a method of another class you would accept this class as an argument and would declare this argument to expect an interface.

Example 2.1. Encapsulated dependency

Test driven design is modern and efficient practice that encourage to build systems with a minimal technical debt. There are many fears and stereotypes about bad influence of testable design on the beauty of OOP, but they are often just misunderstandings. It’s unfortunately not so easy to switch on the development guided by tests and may take a long time, but it’s definitely worth of.

Spread the love