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:

1
2
3
4
5
6
7
8
9
METHOD execute.

  prepare_request_parameters( ).
  
  call_web_service( ). " <- our object seam
  
  do_something_with_results( ).

ENDMETHOD.

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:

1
2
3
4
5
6
7
8
9
CLASS ltc_some_feature DEFINITION
  FOR TESTING
  DURATION SHORT       " Or MEDIUM / LONG
  RISK LEVEL HARMLESS. " Or DANGEROUS / CRITICAL

  PUBLIC SECTION.
    METHOD happy_path FOR TESTING.

ENDCLASS.

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:

1
2
3
4
5
6
7
CLASS lth_some_test_helper_class DEFINITION
  FOR TESTING.

  PUBLIC SECTION.
    CLASS-METHODS create_dummy_response
      RETURNING VALUE(r_result) TYPE REF TO if_rest_entity.
ENDCLASS.

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:

1
2
3
4
5
6
7
8
9
CLASS zcl_some_other_class DEFINITION.
ENDCLASS.

CLASS zcl_some_class DEFINITION
  GLOBAL FRIENDS zcl_some_other_class.
  
  PRIVATE SECTION.
    DATA counter TYPE i.
ENDCLASS.

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
CLASS zth_factory_injector DEFINITION
  FOR TESTING.
  
  PUBLIC SECTION.
    CLASS-METHODS inject_factory_instance
      IMPORTING io_factory TYPE REF TO zif_factory.
  
ENDCLASS.

CLASS zcl_factory DEFINITION
  GLOBAL FRIENDS zth_factory_injector.
  
  PUBLIC SECTION.
    CLASS-METHODS create_instance
      RETURNING VALUE(r_result) TYPE REF TO object.

  PROTECTED SECTION.
    DATA o_factory TYPE REF TO zif_factory.

ENDCLASS.

CLASS zth_factory_injector IMPLEMENTATION.

  METHOD inject_factory_instance.
    zcl_factory=>o_factory = io_factory.
  ENDMETHOD.

ENDCLASS.

CLASS zcl_test_factory DEFINITION
  INHERITING FROM zcl_factory
  FOR TESTING.
  
  PUBLIC SECTION.
    METHODS set_factory_instance
      IMPORTING io_factory TYPE REF TO zif_factory.

ENDCLASS.

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CLASS zcl_dependency_injection DEFINITION.

  PUBLIC SECTION.
    METHODS constructor
      IMPORTING io_factory TYPE REF TO zif_factory.

ENDCLASS.

CLASS zcl_static_factory_method IMPLEMENTATION.

  METHOD add_entry.
    DATA(lo_entry) = zcl_factory=>get_entry( ).
  ENDMETHOD.

ENDCLASS.

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.