Picture of Stefan Lieser
Stefan Lieser

Unit Tests are your Friends

This article is a revised version. It was previously published at refactoring-legacy-code.net published.

Inspired by the discussion with participants of a Clean Code Developer Workshops I once again addressed the question of which strategies I use and recommend for automated testing. The discussion mainly revolved around the question of whether private methods should be tested using unit tests and how this can best be implemented technically. Ostensibly, the challenge is that private methods are not easily accessible to a test. Behind this, however, is the question of whether private methods, i.e. implementation details, should be tested in isolation at all.

To illustrate my point, I would first like to discuss the separation of integration and operation. The Integration Operation Segregation Principle (IOSP) indicates that a method either calls other methods, i.e. integrates them, or contains details. This refers to calls to its own methods, which therefore belong to the solution. Calling framework methods is reserved for operations. Operations have no dependency on other methods of the solution, otherwise they would integrate them. Conversely, integration methods must not contain expressions, API calls, etc., otherwise they would also be operations in addition to their responsibility for integration.

On the one hand, this clear separation of responsibilities leads to easily readable and therefore understandable code. This is the prerequisite for the Changeability. Code that I don't understand, I'd better not change. In the context of automated tests, the IOSP ensures that I can now clearly distinguish between integration and unit tests. A unit test tests a functional unit in isolation. Operations are already isolated. They do not contain any method calls, so they have no dependencies on other methods of their own solution and are therefore units.

However, unit tests alone are not enough to ensure the correctness of a software system. Integration tests are also always required. Only real integration tests can ensure that the functional units to be integrated work correctly when taken together. If all parts to be integrated are replaced by dummies in the integration test, it is no longer an integration test but another unit test. The integration unit is freed from its dependencies and is therefore once again a unit in the test context. However, the test does not find out whether the integration of the real functional units actually works because they have been replaced by dummies.

The test pyramid: system tests, integration tests and unit tests

Automatisiertes-Testen - Clean Code Developer Akademie - Stefan Lieser

This results in the usual test pyramid, consisting of system tests, integration tests and unit tests.

System tests are a variant of integration tests. They test the entire system, including the user interface and resource access. Integration tests typically only run through parts of the system, usually without the user interface.

If a sufficiently large problem is now broken down into smaller components, a hierarchy of methods is created. At the top end is an integration. This integrates other functional units, and finally, at the lowest level, only operations. For the sake of simplicity, I will leave out several levels of integration in the following illustration. Let's take the simple example of a method that calls three operations to complete its task.

Example: ToDictionary

In this example, the method ToDictionary responsible for the three methods SplitIntoSettingsSplitIntoKeyValuePairs and CreateDictionary i.e. to call them in the right way. The three methods called are operations that each deal with one aspect of the problem. They do not call any other methods of the solution, but contain the details, are operations.

In this structure, we can now distinguish between integration tests and unit tests. If the test uses the method ToDictionary is called, this represents an integration test. Finally, the three operations are also executed. Theoretically, it would be possible to call the ToDictionary from its dependencies, then we could write a unit test for the integration. However, the gain in knowledge would be small. It would only show whether the three methods are called in the correct order and the parameters are passed through accordingly. Whether the behavior of the three methods leads to the overall task being solved correctly would not be revealed in this way. This can only be determined by integration testing.

Most developers will probably use the four methods of the ToDictionary problem in the same class. After all, none of the aspects differs so much from the others that this would be a reason to move one of the methods to another class. If, for example, a file were to be accessed in one of the methods, this would be a reason to consider moving this method to another class in order to separate the resource access from the logic.

Private methods

So if all four methods remain in the same class, the implementation details are set to the visibility private want to set. This means that unit tests of the three operations are initially no longer possible, as a test is affected by this restricted visibility in the same way as implementation classes. This is exactly where the discussion begins. In C++, it would be possible to soften the visibility for the purpose of testing, in which the implementation class can use the test class as a friend marked. A colleague has already written about this in the following article:

https://arne-mertz.de/2015/08/unit-tests-are-not-friends/

I share the criticism of friend for the purpose of testability. Unfortunately, it is an omission of all existing programming languages that there is no difference between public and private there is no visibility that expresses that the symbol is private, but at the same time should be visible for tests. Something like public for tests is what we are looking for here. In Swift there is the testable importwhich, however, only makes internal information visible, not private methods.

So what we are looking for is a technical workaround that makes private methods accessible for tests. In .NET you can use internal in combination with the attribute InternalsVisibleTo can be used as a compromise, see my Contribution to the topic Visibility. In C++ you can friend can be used.

But these are technical answers that do not address the question of whether it makes sense to test private details of a class at all. Let's look again at the ToDictionary Example. If we were to outsource the three operations to a separate class, these methods would inevitably be publicso that they can be used and integrated from the other class. In this case, nobody would be bothered by the fact that the operations are tested in isolation. After all, they are public. But what is the difference to operations that are only private because they belong to the same aspect as integration? 

Of course, the potential dependencies are reduced if methods are kept private. This is the reason why the three operations should be in the same class as their integration ToDictionary. Nevertheless, the desire to test them in isolation remains. The separation into integration and unit tests (see test pyramid) is the core of any test strategy: The integration tests are used to test a few "bread and butter" cases, the unit tests are responsible for the many details. Testing everything with integration tests would mean a combinatorial explosion.

Black Box Tests - Clean Code Academy - Stefan Lieser

Black box vs. white box

Another argument is that automated tests should not know the details of the implementation. These so-called Black box tests view the implementation exclusively via its public interface. Of course, this has the advantage that the black box tests do not have to be modified if extensive changes are made to the details. On the other hand, however, these tests are generally more complex, as they can test all aspects of the solution exclusively via the public interface.

I think we should not obstruct the possibility of a simple test structure by nailing down private methods. Of course, we have to make sure that nobody tampers with private methods in the rest of the implementation and uses them. That is not what they are intended for. If all methods are considered public, this results in a higher coupling. This problem would be avoided if public and private the visibility testable would be introduced. It would then be clear to every developer that these methods may not be used. 

The compiler and the IDE would point this out. The methods could still be called in the test. This would make it easy to implement a test strategy consisting of a few integration tests and many unit tests. In the meantime, I'll content myself with the technical workarounds and make private details about internalfriend or similar mechanisms. Correctness and therefore automated tests have a very high priority. Pragmatism is required instead of dogmatically excluding private methods from tests.

Our seminars

course
Clean Code Developer Basics

Principles and tests - The seminar is aimed at software developers who are just starting to deal with the topic of software quality. The most important principles and practices of the Clean Code Developer Initiative are taught.

to the seminar "
course
Clean Code Developer Trainer

Conducting seminars as a trainer - This seminar is aimed at software developers who would like to pass on their knowledge of Clean Code Developer principles and practices or Flow Design to others as a trainer.

to the seminar "
course
Clean Code Developer CoWorking

Online CoWorking incl. coaching -
We are often asked what developers can do to keep up with the topic of clean code development. Our answer: Meet up with other Clean Code Developers online on a regular weekly basis.

to the seminar "

Leave a Comment

Your email address will not be published. Required fields are marked *

en_USEnglish