You are on page 1of 20

What is it

Quick Demo Video

Getting started

Configuring outputs for method calls

Configuring exceptions for method calls

Configuring events for method calls

Changing method call behavior based upon the number of calls

Verifying Interactions

Advanced Topics

Implementing Custom Matchers

Implementing Custom Answers

Dependent objects which cant be controlled makes writing unit tests hard or even impossible. In
unit test environments dependent objects should be replaced with test doubles. They imitate the
behavior of the real objects. The graphic below illustrates this idea.

Until now test doubles classes had to be written by hand. This can be quite a tedious process. The
ABAP Test Double Framework solves this problem and makes it easier to write unit tests for your
code.
The framework is available with SAP BASIS release 740 SP9 and higher.
What is it
The ABAP Test Double Framework is a standardized solution for creating test doubles and
configuring their behavior. The framework supports the automatic creation of test doubles for global
interfaces. Method call behavior can be configured using the framework API. It is possible to define
the values of returning, exporting, changing parameters and raise exceptions or events. Additionally,
the framework provides functionality to verify interactions on the test double object, e.g. the number
of times a method was called with specific input parameters.

Quick Demo Video


Getting started
In this document we use an expense management application as an
example. cl_expense_manager is one of the main classes in the application which is used for
expense calculations. Expenses can be entered by the users in different currencies and the expense
manager has methods to calculate the total expense in the required currency. The expense manager
uses an object of if_td_currency_converter to get the real time currency conversion rates and then
calculate the total expenses. For testing methods of cl_td_expense_manager, we have to make
sure that the method calls on the if_td_currency_converter object return exactly the values that we
expect it to return. Otherwise the unit test would fail because the values on which assertions are
being done are dependent on the values returned by the methods of
the if_td_currency_converter interface. To achieve this, first we have to create a test double object
for the if_td_currency_converter interface and inject it into the expense calculation class.
We will be using the if_td_currency_converter interface as the external api interface for which test
doubles get created, throughout this document.
The example interface
INTERFACE if_td_currency_converter PUBLIC .
EVENTS new_currency_code EXPORTING VALUE(currency_code) TYPE string.
METHODS convert
IMPORTING
amount TYPE i
source_currency TYPE string
target_currency TYPE string
RETURNING VALUE(result) TYPE i
RAISING cx_td_currency_exception.
METHODS convert_to_base_currency
IMPORTING
amount TYPE i
source_currency TYPE string
EXPORTING
base_currency TYPE string
base_curr_amount TYPE i.
ENDINTERFACE.
Lets get started with the creation and the injection of the test double object.
Creating and Injecting the test double instance
CLASS ltcl_abap_td_examples DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS:
create_double FOR TESTING RAISING cx_static_check,
ENDCLASS.
CLASS ltcl_abap_td_examples IMPLEMENTATION.
METHOD create_double.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO cl_td_expense_manager.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
injecting the test double into the object being tested
CREATE OBJECT lo_expense_manager EXPORTING currency_converter =
lo_currency_converter_double.
ENDMETHOD.
ENDCLASS.
Please note that casting the test double object to the correct variable reference is very important.
The example shows the injection of the test double object through the constructor, you can also use
any other form of dependency injection.

Configuring outputs for method calls


The next step is to configure the behavior of the methods of the test double. We can configure
specific output values for specific input values. The configuration of method call consists of two
statements in sequence. The first statement is the configuration call. Its primarily used by
configuring the output values. The second statement is a call on the double object. Its used to
specify the input parameters.
The following example shows a simple configuration which specifies that 80 should be returned by
the double if the convert method call gets called with the input: amount = 100 , source_currency =
USD and target_currency = EUR.
Simple configuration
METHOD simple_configuration.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO
cl_td_expense_manager,
lv_total_expense TYPE i.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
configuration for stubbing method convert:
step 1: set the desired returning value for the method call
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>returning( 80 ).
step 2: specifying which method should get stubbed
lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = USD
target_currency = EUR
).
injecting the test double into the object being tested
CREATE OBJECT lo_expense_manager EXPORTING currency_converter =
lo_currency_converter_double.
add one expense item
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 1
currency_code = USD
amount = 100
).
actual method call
lv_total_expense = lo_expense_manager-
>calculate_total_expense( currency_code = EUR ).
assertion
cl_abap_unit_assert=>assert_equals( exp = 80 act = lv_total_expense ).
ENDMETHOD.
The code inside the method calculate_total_expense calls the convert method
of if_td_currency_converter. In the example the calls to the convert method always return 80 for
the specified input parameters. By using a test double we make sure that currency conversion
fluctuations in the real world does not affect our unit tests.
Example of different variants of configurations:
METHOD configuration_variants.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO
cl_td_expense_manager,
lv_total_expense TYPE i.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
eg1: configuration for exporting parameters
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>set_parameter( name = base_currency value = EUR
)-
>set_parameter( name = base_curr_amount value = 80 ).
lo_currency_converter_double->convert_to_base_currency(
EXPORTING
amount = 100
source_currency = USD
).
eg2: configuration ignoring one parameter. 55 gets returned if source
currency = USD , target currency = EUR and any value for amount.
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>returning( 55 )->ignore_parameter( amount ).
lo_currency_converter_double->convert(
EXPORTING
amount = 0 dummy value because amount is a non optional
parameter
source_currency = USD
target_currency = EUR
).
eg3: configuration ignoring all parameters. 55 gets returned for any
input
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>returning( 55 )->ignore_all_parameters( ).
lo_currency_converter_double->convert(
EXPORTING
amount = 0 dummy value
source_currency = USD dummy value
target_currency = EUR dummy value
).
ENDMETHOD.
Please note that the configure_call method is used to configure the next method call statement on
the test double. If you need to configure different methods of an interface,
the configure_call method should be called for every method.

Configuring exceptions for method calls


We can configure exceptions to be raised for a method call with specific input parameters. To
configure an exception, the exception object to be raised has to be instantiated and then added to
the configuration statement.
METHOD configuration_exception.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO cl_td_expense_manager,
lv_exp_total_expense TYPE i,
lo_exception TYPE REF TO
cx_td_currency_exception.
FIELD-SYMBOLS: <lv_value> TYPE string.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
instantiate the exception object
CREATE OBJECT lo_exception.
configuration for exception. The specified exception gets raised if
amount = -1, source_currency = USD and target_currency = EUR
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>raise_exception( lo_exception ).
lo_currency_converter_double->convert(
EXPORTING
amount = -1
source_currency = USD
target_currency = EUR
).
ENDMETHOD.
Limitation:
Only class based exceptions are supported.

Configuring events for method calls


Events can be configured to be raised for method calls . The event name along with parameters and
values (if any) has to be specified in the configuration statement.
METHOD configuration_event.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO cl_td_expense_manager,
lv_total_expense TYPE i,
lv_exp_total_expense TYPE i,
lt_event_params TYPE abap_parmbind_tab,
ls_event_param TYPE abap_parmbind,
lo_handler TYPE REF TO lcl_event_handler.
FIELD-SYMBOLS: <lv_value> TYPE string.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
configuration for event. new_currency_code event gets raised if the
source_currency = INR
ls_event_param-name = currency_code.
CREATE DATA ls_event_param-value TYPE string.
ASSIGN ls_event_param-value->* TO <lv_value>.
<lv_value> = INR.
INSERT ls_event_param INTO TABLE lt_event_params.
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>raise_event( name = new_currency_code parameters = lt_event_params
)-
>ignore_parameter( target_currency
)-
>ignore_parameter( amount ).
lo_currency_converter_double->convert(
EXPORTING
amount = 0
source_currency = INR
target_currency =
).
ENDMETHOD.
CLASS lcl_event_handler DEFINITION.
PUBLIC SECTION.
DATA: lv_new_currency_code TYPE string.
METHODS handle_new_currency_code FOR EVENT new_currency_code OF
if_td_currency_converter IMPORTING currency_code.
ENDCLASS.
CLASS lcl_event_handler IMPLEMENTATION.
METHOD handle_new_currency_code.
lv_new_currency_code = currency_code.
ENDMETHOD.
ENDCLASS.
Limitation:
Default values for event parameters are not supported. If an event has to be raised with a default
value for a parameter, the value has to be explicitly specified in the configuration statement.

Changing method call behavior based upon


the number of calls
In the previous examples, we have learned how to configure the output parameters for a specific
combination of input parameters. This means that the configured output gets returned by the test
double for any number of calls to the method with the specified input parameters. But there can be
cases where the output may have to change depending on the number of calls. This can be
achieved with adding the times method to the configuration statement.
Configuring method call behavior for a specific number of calls
METHOD configuration_times.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO
cl_td_expense_manager,
lv_total_expense TYPE i.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
configuration for returning 80 for 2 times
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>returning( 80 )->times( 2 ).
lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = USD
target_currency = EUR
).
configuration for returning 40 the next time
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>returning( 40 ).
lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = USD
target_currency = EUR
).
ENDMETHOD.
The previous example configures the double to return 80 for the first two calls and then 40 for the
third call on the method.
Please note the following behavior for the configurations:
1. If times is not specified in the configuration, it is implied to be 1.
2. If a call comes exceeding the number of times specified, then the output of the last matching
configuration is returned. For example, in the above example 40 would be returned for 4th call.

Verifying Interactions
We can also set expectations on the interactions on the test double and verify these expectations at
the end of the test. This is helpful in conditions when the number of calls of specific interface
methods needs to be tracked. The conditions to be verified can be configured by
chaining and_expect method in the configuration statement. The actual verification against the
expectations is done at the end of the test with the verify_expectations method call.
METHOD verify_interaction.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO cl_td_expense_manager,
lv_total_expense TYPE i,
lv_exp_total_expense TYPE i VALUE 160.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
injecting the test double into the object being tested
CREATE OBJECT lo_expense_manager EXPORTING currency_converter =
lo_currency_converter_double.
add three expenses
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 1
currency_code = USD
amount = 100
).
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 2
currency_code = USD
amount = 100
).
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 3
currency_code = INR
amount = 100
).
configuration of expected interactions
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>returning( 80 )->and_expect( )->is_called_times( 2 ).
lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = USD
target_currency = EUR
).
actual method call
lv_total_expense = lo_expense_manager-
>calculate_total_expense( currency_code = EUR ).
assertion
cl_abap_unit_assert=>assert_equals( exp = lv_exp_total_expense act =
lv_total_expense ).
verify interactions on testdouble

cl_abap_testdouble=>verify_expectations( lo_currency_converter_double ).
ENDMETHOD.
In the previous example, the method verify_expectations will raise an error message if
the convert method gets called less than 2 times. The framework always raises error messages as
early as possible. If the convert method gets called for the 3rd time it would immediately raise a
message and wont wait till the verify_expectations method gets executed.

Advanced Topics
Implementing Custom Matchers
The framework always matches the configured behavior with actual calls by using a default
implementation of the if_abap_testdouble_matcher interface. It uses the ABAP EQ operator for
matching the input parameters. However, in some use cases this may not be sufficient. The
framework supports custom matchers where a user can configure how the input values get
evaluated by the framework. This functionality is important if there are objects passed as arguments
to a method or if there has to be some user specific logic for matching. Custom matchers have to
implement the if_abap_testdouble_matcher interface. The matcher object gets used by adding
the set_matcher method in the configuration statement. It tells the framework to replace the default
matcher with the custom matcher. When an actual method call happens the framework calls
the match method passing the actual arguments and the configured arguments for all methods to
evaluate equality. The custom matcher provides the logic to evaluate the equality in
the match method.
Custom matcher class implementation
CLASS lcl_my_matcher DEFINITION.
PUBLIC SECTION.
INTERFACES if_abap_testdouble_matcher.
ENDCLASS.
CLASS lcl_my_matcher IMPLEMENTATION.
METHOD if_abap_testdouble_matcher~matches.
DATA : lv_act_currency_code_data TYPE REF TO data,
lv_conf_currency_code_data TYPE REF TO data.
FIELD-SYMBOLS:
<lv_act_currency> TYPE string,
<lv_conf_currency> TYPE string.
IF method_name EQ CONVERT.
lv_act_currency_code_data = actual_arguments-
>get_param_importing( source_currency ).
lv_conf_currency_code_data = configured_arguments-
>get_param_importing( source_currency ).
ASSIGN lv_act_currency_code_data->* TO <lv_act_currency>.
ASSIGN lv_conf_currency_code_data->* TO <lv_conf_currency>.
IF <lv_act_currency> IS ASSIGNED AND <lv_conf_currency> IS ASSIGNED.
IF <lv_act_currency> CP <lv_conf_currency>.
result = abap_true.
ENDIF.
ELSE.
result = abap_false.
ENDIF.
ENDIF.
ENDMETHOD.
ENDCLASS.
Using the custom matcher in a configuration
METHOD custom_matcher.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO cl_td_expense_manager,
lv_total_expense TYPE i,
lv_exp_total_expense TYPE i VALUE 160,
lo_matcher TYPE REF TO lcl_my_matcher.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
configuration
CREATE OBJECT lo_matcher.
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>returning( 80 )->set_matcher( lo_matcher ).
lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = USD*
target_currency = EUR
).
injecting the test double into the object being tested
CREATE OBJECT lo_expense_manager EXPORTING currency_converter =
lo_currency_converter_double.
add expenses with pattern
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 1
currency_code = USDollar
amount = 100
).
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 2
currency_code = USDLR
amount = 100
).
actual method call
lv_total_expense = lo_expense_manager-
>calculate_total_expense( currency_code = EUR ).
assertion
cl_abap_unit_assert=>assert_equals( exp = lv_exp_total_expense act =
lv_total_expense ).
ENDMETHOD.

Implementing Custom Answers


The framework provides the possibility to completely influence the output/result of a method call with
user specific coding. The custom answer has to implement
the if_abap_testdouble_answer interface and the answer object gets used by adding
the set_answer method to the configuration statement. The answer method is called by the
framework when the input values of an actual method call matches a configuration.
The answer method has all the logic to provide the output values. Exporting, changing, returning
values and exceptions can be set on the result object. Furthermore, events can be raised using
the double_handle object.
Custom answer class implementation
CLASS lcl_my_answer IMPLEMENTATION.
METHOD if_abap_testdouble_answer~answer.
DATA : lv_src_currency_code_data TYPE REF TO data,
lv_tgt_currency_code_data TYPE REF TO data,
lv_amt_data TYPE REF TO data,
lt_event_params TYPE abap_parmbind_tab,
ls_event_param TYPE abap_parmbind.
FIELD-SYMBOLS:
<lv_src_currency_code> TYPE string,
<lv_tgt_currency_code> TYPE string,
<lv_amt> TYPE i,
<lv_value> TYPE string.
IF method_name EQ CONVERT.
lv_src_currency_code_data = arguments-
>get_param_importing( source_currency ).
lv_tgt_currency_code_data = arguments-
>get_param_importing( target_currency ).
lv_amt_data = arguments->get_param_importing( amount ).
ASSIGN lv_src_currency_code_data->* TO <lv_src_currency_code>.
ASSIGN lv_tgt_currency_code_data->* TO <lv_tgt_currency_code>.
ASSIGN lv_amt_data->* TO <lv_amt>.
IF <lv_src_currency_code> IS ASSIGNED AND <lv_tgt_currency_code> IS
ASSIGNED AND <lv_amt> IS ASSIGNED.
IF <lv_src_currency_code> EQ INR AND <lv_tgt_currency_code> EQ
EUR.
result->set_param_returning( <lv_amt> / 80 ).
ENDIF.
ENDIF.
ENDIF.
ENDMETHOD.
ENDCLASS.
Adding the custom answer implementation to a method call configuration
METHOD custom_answer.
DATA: lo_currency_converter_double TYPE REF TO
if_td_currency_converter,
lo_expense_manager TYPE REF TO
cl_td_expense_manager,
lv_total_expense TYPE i,
lv_exp_total_expense TYPE
i VALUE 25,
lo_answer TYPE REF TO lcl_my_answer.
create test double object
lo_currency_converter_double ?=
cl_abap_testdouble=>create( if_td_currency_converter ).
instantiate answer object
CREATE OBJECT lo_answer.
configuration
cl_abap_testdouble=>configure_call( lo_currency_converter_double )-
>ignore_parameter( amount )->set_answer( lo_answer ).
lo_currency_converter_double->convert(
EXPORTING
amount = 0
source_currency = INR
target_currency = EUR
).
injecting the test double into the object being tested
CREATE OBJECT lo_expense_manager EXPORTING currency_converter =
lo_currency_converter_double.
add the expense line items
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 1
currency_code = INR
amount = 80
).
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 2
currency_code = INR
amount = 240
).
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 3
currency_code = INR
amount = 800
).
lo_expense_manager->add_expense_item(
EXPORTING
description = Line item 4
currency_code = INR
amount = 880
).
actual method call
lv_total_expense = lo_expense_manager-
>calculate_total_expense( currency_code = EUR ).
assertion
cl_abap_unit_assert=>assert_equals( exp = lv_exp_total_expense act =
lv_total_expense ).
ENDMETHOD.
The framework currently supports the creation of test doubles for global interfaces. Support for non-
final classes is already under discussions.

Have a look at the framework and feel free to give your feedback or ask questions in the comment
section.

Dependency Injection for ABAP


August 28, 2013 | 148 Views |

Jack Stewart

more by this author


ABAP Development
abapabap objectsabap oocodedependency injectiondesign patternsooOpen
Sourcereusabilitysaptesting;unit-testing

share 0

share 0

tweet

share 0

Like1
Follow

Dependency Injection for ABAP:


Loose Coupling and Ease of Testing
Overview
What Is Dependency Injection?
Unit testing is often made more dicult by the heavy use of classes as namespaces and the
proliferation of static methods to encapsulate conguration code.
Dependency injection is a design pattern that shifts the responsibility of resolving dependencies to a
dedicated dependency injector that knows which dependent objects to inject into application code.
Dependency injection oers a partial solution to our problem, by oering an elegant way to plug in
either the new objects taking over the responsibilities of static methods, or others required for testing
purposes.
[Niko Schwarz, Mircea Lungu, Oscar Nierstrasz, Seuss: Decoupling responsibilities from static
methods for fine-grained configurability, Journal of Object Technology, Volume 11, no. 1 (April
2012), pp. 3:1-23, doi:10.5381/jot.2012.11.1.a3.]

Why Use Dependency Injection?


Some of the benefits of dependency injection in the ABAP context include:

Loose coupling of code

o An application knows about an interface, and only deals with the interface. The
dependency injection system is responsible for hooking the interface reference up to
an actual instance of functionality.

Ease of testing

o The dependency injection system can be triggered so that it only instantiates test
implementations and mock functionality. This means a developer can test the real
system with some functionality switched out, easing testing of parts of a system.

In a real world scenario, dependency injection could be used in parts of an application such as
accessing configuration data or handling reading and writing of application data to the database. In a
testing scenario, we need to test using a specific configuration for predictable results. We also may
not want to save data on the database, and we may depend on specific data being used for tests.
Using dependency injection, we can ensure a known and stable configuration is used during the test,
and use specific mock data provided instead of data directly from the database, to make sure the
data is processed as required.

Dependency Injection in Action


Using RTTI and some Object Oriented Data Dictionary metadata stored within SAP, a
straightforward and easy to use approach can be taken to implement this functionality. In this section
we have code snippets outlining the approach taken, including UML diagrams. The entire working
example can also be downloaded for installation and experimentation.

An Example Report
We will start with an example of dependency injection in action using a basic report. For our simple
program, we have an interface containing some business logic, in this case configuration data,
called zdi_if_config. We have several implementations of the interface, each of which is intended
for a different scenario. For everyday use, we want our application to instantiate the default class,
zdi_if_config_def. Another department would like the functionality to behave a little differently, but
we dont want to go straight in and modify our default class, so we create zdi_if_config_cust which
descends from the default class and replaces some of the functionality. Finally, when were testing
our application we want to modify how the default class works, returning test data so we can be sure
the environment is not affecting how the application runs for example, providing a static
configuration so we know exactly what behaviour to expect when testing. For this purpose, we have
the zdi_if_config_test class.
[UMLRelationshipHierarchy.png]
The application itself is not responsible for instantiating instances of our interface this is passed off
to the dependency injection system, which makes a decision from the available implementations of
an interface and instantiates an instance of a class accordingly.

Note: It is worth highlighting the fact that despite discussing the different implementing classes above, the program
itself knows nothing of these implementations, and contains no references at all to these types. From the point of
view of the program, the interface zdi_if_config is the only known type and the only type dealt with. This is one of
the benefits of dependency injection the program is now very loosely coupled to the implementation of the
functionality. All the program needs to know is that theres a contract for some behaviour it needs, how that contract is
fulfilled doesnt matter to the program.

For this example, we have a data variable which references our interface. The dependency injection
system will automatically create an instance for the variable, the underlying functionality depending
on the scenario (in this case, a declaratively defined mode.)
data:
lif_config type ref to zdi_if_config.

We start in standard mode here we always use the default class unless an alternative
implementation has been specified. We will inject an instance into the variable, and the expected
outcome is a reference containing an instance of the alternative class.
inject:
lif_config. instantiates alternative class

In this case, lif_config will contain an instance of zdi_if_config_cust. Next, we will go into test
mode, and repeat the same procedure. When asking for a reference to our interface, the system
finds the test class and instantiates this.
clear lif_config.
test_mode. indicates we want a class tagged with test tag
inject:
lif_config. instantiates test class

In this case, lif_config will contain an instance of zdi_if_config_test. Finally, we will go into pure
mode. Here, we want to ignore any alternative implementations and use only default
implementations. Pure mode is useful for when we have developed alternative implementations, but
need to use the original default implementation for some reason.
clear lif_config.
pure_mode. indicates we want a class tagged with default tag
inject:
lif_config. instantiates default class
In this case, lif_config will contain an instance of zdi_if_config_def.

Note: The different modes have been declaratively defined, but they could just as easily be configured, allowing
dynamic alteration of how the application instantiates instances without code changes. The actual functionality to use
could also be configured, for example specifying that a certain class should be instantiated when a specific user
requests an instance for a specific interface.

Unit Testing

In normal operation, the injection would be performed only once per scope. The functionality may be
needed in several places throughout the application, and the variable would be injected in all the
places required. If a test scenario is needed, the DI test mode would be entered once at the
beginning of the unit test (during set-up) and then the application objects would be instantiated.
Every time the application requested an instance for the configuration, a test instance would be
provided by the DI.
class bigapp_app_unit_test definition for testing.
private section.
class-data:
mif_app type ref to zbigapp_if_cntrl.
classmethods:
class_setup.
methods:
test_printing for testing.
endclass.
class bigapp_app_unit_test implementation.
method class_setup.

data:
lif_mod type ref to zbigapp_if_model.

* safe in the knowledge that the app will use the test configuration
* and any other test injectables we have, such as database persistence
* overriding classes or a dummy user interface implementation.
test_mode.

* instantiate the application model. The injection is performed

* inside of this call.

lif_mod = zbigapp_cl_model_fact=>load(
exporting
i_key = 0042/DMO/000/00 ).

* instantiate the application controller.


mif_app = zbigapp_cl_cntrl_fact=>create(
exporting
i_model = lif_mod ).

endmethod.

method test_printing.
* make sure the test device is configured for the app.
* the real IMG configuration may have an alternative value
* for the current user, but the test configuration class
* overrides this so weuse a specific device during testing.

cl_aunit_assert=>assert_equals(
act = mif_app->m_device
exp = TEST_DEVICE
msg = Test device was not configured for application).

* tell the application to print some data.


mif_app->action( zbigapp_if_cntrl=>mcc_print ).

* check our test device to see whether the data was ok.
cl_aunit_assert=>assert_equals(
act = zbigapp_test_device=>m_printdata_ok( )
exp = X
msg = Test device reports print data was not ok).
endmethod.
endclass.

Implementation Approach
Here we will look at the implementation of the injection functionality. The inject command is simply
a macro that passes the reference along to a static class method called
zdi_cl_injection=>create_inst( ). The single changing parameter is called c_ref, of type any.

method create_inst.

data:
li_ifcl_items type mt_classlist_tt,
lvc_ifcl_item type mt_classitem,
li_tags type mt_taglist_tt,
lr_tag type mt_tagitem,
lr_map type mt_classmap,
lcl_descr_ref type ref to cl_abap_refdescr,
lcl_abap_typdscr type ref to cl_abap_typedescr.

First, we get some type information using the SAP RTTI system. We need to determine the type of
the variable passed in, to make sure its a reference and get the dictionary name of the underlying
interface or class.

* determine the dictionary type of the reference that was passed in.
lcl_descr_ref ?= cl_abap_refdescr=>describe_by_data( c_ref ).
lcl_abap_typdscr = lcl_descr_ref->get_referenced_type( ).

lvc_ifcl_item = lcl_abap_typdscr->get_relative_name( ).

if lvc_ifcl_item is initial.
raise nondictionary_type.
endif.

Next, we check our cache of previously requested instances. If a class has already been determined
for this functionality, then we simply instantiate the class and return the instance.

* check our cache..


read table mi_cache into lr_map
with key source = lvc_ifcl_item.
if sy-subrc eq 0.
create object c_ref type (lr_map-target).
return.
endif.

lr_map-source = lvc_ifcl_item.

Next, we vary behaviour depending on whether the reference passed in is a reference to a class of
interface. We are building up a list of potential instance candidates, so when a class is passed in we
simply add it to the list (and well look up descendants in a later step.) If an interface was passed in,
we fetch a list of all classes that implement the interface (using table vseoimplem.)

* input can be a class or an interface whose imping classes we want.


case lcl_abap_typdscr->type_kind.

when cl_abap_typedescr=>typekind_class.

append lvc_ifcl_item to li_ifcl_items.

when cl_abap_typedescr=>typekind_intf.

get_imp_classes( classes that implement the passed in interface


exporting
i_interface = lvc_ifcl_item
importing
e_classes = li_ifcl_items
).

when others.
raise not_a_reference.
endcase.

Next, we explode the dependency tree of the classes we have collected (using table vseoextend.)
This is required as the technique we use for selecting implementers of an interface only returns the
classes directly implementing an interface, and not the descendants which do not explicitly define
the implementation of the interface. Also, if a class reference was passed in then we need to fetch
the potential candidates, which would consist of descendants of the class.
explode_descendents( get entire descendant tree.
changing c_classlist = li_ifcl_items ).

if li_ifcl_items is initial.
raise no_implementing_classes.
endif.

Next, we analyse the class list and determine any tags associated with them (using table
seotypepls, which contains the forward declaration list which we have hijacked for tagging!)
Possible tags include being flagged as a default class, an alternative class or a test class.

fetch_class_tags(
exporting
i_classlist = li_ifcl_items
importing
e_taglist = li_tags
).

Next, we apply our selection logic if we are in test mode, we look for a test class; if we are in pure
mode we look for a default class; otherwise we look for an alternative class, failing that a default
class, and failing that the first class in the list.
do.
case mvb_mode.
when mcc_mode_test.
* if were in test mode, read first test class if exists.
get_target_with_tag mcc_tag_tst.
when mcc_mode_pure.
* if were in pure mode, read first default class if exists.
get_target_with_tag mcc_tag_def.
endcase.

* see if an alternative class exists.


get_target_with_tag mcc_tag_alt.

* see if we have a class marked as standard.


get_target_with_tag mcc_tag_def.

* fall through, when no keywords found just return first class.


read table li_ifcl_items into lvc_ifcl_item
index 1.
exit.
enddo.

if lvc_ifcl_item is initial.
raise no_implementing_classes.
endif.

Next, we instantiate the class we want to use, and put the reference into c_ref.

try.
create object c_ref type (lvc_ifcl_item).
catch cx_sy_create_object_error.
raise instantiation_failed.
endtry.

Finally, we cache the name of the class and map it to the name of the type of the reference that was
passed in.

lr_map-target = lvc_ifcl_item.
append lr_map to mi_cache.

endmethod.

Downloads
Files containing the implementation can be found on the project page:
https://bitbucket.org/zmob/di/downloads. This includes two SAPLink nuggets. Install
NUGG_ZDI_IF.nugg first youll need the interface extension for SAPLink; then install
NUGG_ZDI.nugg.

Next Steps (Future Articles)

Injecting constructor parameters on the fly

Configuration based selection of implementing functionality

Containers for managing lifecycle of objects


-

Test Report
report zdi_prog.
data:
lvs_msg type string.
define inject.
*> &1 ref to interface or class
zdi=>create_inst(
changing c_ref = &1 ).
assert &1 is not initial.
end-of-definition.
define test_mode.
zdi=>test_mode( ).
end-of-definition.
define pure_mode.
zdi=>pure_mode( ).
end-of-definition.
define prn.
*> &1 obj to test
lvs_msg = &1->test( ).
write lvs_msg.
new-line.
end-of-definition.
start-of-selection.
* we have the business logic interface:
* ZDI_IF
* we have business logic classes implementing ZDI_IF:
* default (standard, from us) ZDI_DEFAULT
* alternative (customer) ZDI_ALTERNATIVE
* test (for units) ZDI_TEST
data:
lcl_di1 type ref to zdi_default,
lif_di2 type ref to zdi_if,
lcl_di3 type ref to zdi_default,
lif_di4 type ref to zdi_if,
lif_di5 type ref to zdi_if.
* the business logic class that is instantiated varies depending on the
* mode we are in. Within Unit tests, the test mode could be activated
* and any configuration and persistence classes could be switched out.
inject:
lcl_di1, instantiates alternative class ZDI_ALTERNATIVE
lif_di2. instantiates alternative class ZDI_ALTERNATIVE
test_mode. indicates we want a class tagged with test tag
inject:
lcl_di3, instantiates alternative class ZDI_ALTERNATIVE
lif_di4. instantiates test class ZDI_TEST
pure_mode. indicates we want a class tagged with default tag
inject:
lif_di5. instantiates default class ZDI_DEFAULT
prn:
lcl_di1, alternative alternative is a descendant of default
lif_di2, alternative alternative implements interface
lcl_di3, alternative test class is not related to default
lif_di4, test test implements interface
lif_di5. default default implements interface and is tagged def

You might also like