Friends with ABAP Test Classes
Contents
Both Feathers (2005) and van Deursen & Seemann (2019) recommend that test code should not be mixed with production code and that production code should avoid testing-specific features (such as overriding values). van Deursen and Seemann give away their suggested solution in the title of the book – Dependency Injection –, while Feathers argues that techniques like Introduce Static Setter (2005, 372) are sometimes necessary to in order to make code that depends on singletons testable.
Between them, and likely because even the title of Feathers' book is Working Effectively with Legacy Code, Dependency Injection is more aspirational of the two: whereas Feathers leans heavily into object (and other) seams, van Deursen and Seemann talk about how dependency injection, when properly practiced, is enough to support both good design and testing.
Recently SAP Press published an E-Bite Designing Testable ABAP Classes and Packages by Winfried Schwarzmann. Instead of relying on the tips described by Feathers, it introduces an ABAP-specific way of creating test-enabling code that is strongly isolated from productive code. The method uses friend classes which, somewhat interestingly, were never mentioned in Feathers' book.
Seams
A seam, in the terminology of Feathers, is “a place where you can alter behavior in your program without editing in that place” (2005, 31). Although the book lists a few different kinds of seams, only object seams are relevant to ABAP. A simple object seam could be something like the following:
|
|
Because the web service call takes place in its own method, it could be overridden in a subclass and thus execute
method could be tested without actually contacting any external server. Seams are used to avoid volatile dependencies or methods with side effects and they can make it easier to emulate and test various errors which could be troublesome to create using an actual external server.
Test classes in ABAP
This is a basic test class definition in ABAP:
|
|
The additions of FOR TESTING
, DURATION SHORT
, RISK LEVEL HARMLESS
in the class definition and FOR TESTING
in the method tell ABAP Unit that the class contains test cases to be run.
Some of these additions can be dropped for the following effect:
|
|
Now the class has only FOR TESTING
addition. This causes the class to be a “test class” even though it does not contain any executable tests. It can be used like any other class in test context, but it cannot be addressed from productive code (ie. non-test code). Schwarzmann relies on this property of test classes to introduce an alternative to Feathers' Introduce Static Setter.
Friends
Classes in ABAP can declare other classes as their friends. This breaks the normal object-oriented encapsulation, allowing the class that was declared as the friend to access all attributes and methods of the class that did the declaring:
|
|
In the above case, zcl_some_other_class
can access the private attribute counter
of class zcl_some_class
. Schwarzmann uses class friendships to allow the behavior of an otherwise static factory class to be altered:
|
|
Note that for this setup to be of any use, zcl_factory
has to be a singleton or in some other way delegate its static method calls to an instance which can then be replaced (o_factory
).
In the above example, zth_factory_injector
class allows the singleton zcl_factory
to be replaced with an instance better suited for testing. And because the zth_factory_injector
is declared as a test class (FOR TESTING
), we can be sure that production code cannot interfere with zcl_factory
singleton.
Compared to Feathers' Introduce Static Setter, this introduces a language-level restriction for abusing functionality that was created to enable testing (2005, 372). If zcl_factory
is not declared as FINAL
, we could also use a subclass with a static setter and declared it a test class to arrive at more or less the same situation.
Subclassing in ABAP is restricted to single inheritance, however. If there were more singletons that we would like to replace for testing, we would need to subclass each one, whereas a single injector class could be made a friend of each and every one.
Friends vs. Inheritance
Schwarzmann does not use a friend class to only allow a singleton to be replaced in test cases, but also exposes the injector class as part of an ABAP package interface. The injector is thus meant to allow code that depends on the package to replace the normal behavior with test doubles. And, if the package exposes multiple classes that all would benefit from mocking for tests, using a single injector class would avoid having to create a testing-specific subclass for each exported class.
Downside
This not really a downside of the friend approach, specifically. Rather it is the result of relying on static factory methods instead of a composition root and dependency injection (though static factory methods and dependency injection are not mutually exclusive).
The main shortcoming of using static factory methods rather than dependency injection is that they obscure the actual dependencies of the class. Consider the following
|
|
zcl_dependency_injection
clearly declares that it depends on an instance of zif_factory
to execute its work. zcl_static_factory_method
uses a static factory method in a method, obscuring the fact that the class depends on another class and making it difficult to intercept the dependency.
Wrapping up
Before reading the E-Bite, I did have an understanding that FOR TESTING
/ test classes were not part of the production runtime. I had subclassed some troublesome/volatile classes and declared those as FOR TESTING
. But I had not though about creating a global test class that would enable the behavior of normal public classes to be altered via class friendships or that test helper classes should be exported for use by other packages, as I had created almost exclusively local test classes.
The following should be noted from ABAP documentation: “Currently, all instance methods of a global test class are automatically test methods.” Ie. whatever convenience functions are created, they should be static.
Sources
- Feathers, M (2005) Working effectively with legacy code. Prentice Hall Professional Technical Reference. 978-0-13-117705-5.
- Schwarzmann, W (2022) Designing Testable ABAP Classes and Packages. Rheinwerk Publishing. 978-1-4932-2218-6.
- van Deursen, S. – Seemann, M. (2019) Dependency Injection: principles, practices, and patterns. Manning Publications. 978-1-61729-473-0.