Professional Documents
Culture Documents
Getting started
Verifying Interactions
Advanced Topics
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.
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.
Have a look at the framework and feel free to give your feedback or ask questions in the comment
section.
Jack Stewart
share 0
share 0
tweet
share 0
Like1
Follow
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.
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.
lif_mod = zbigapp_cl_model_fact=>load(
exporting
i_key = 0042/DMO/000/00 ).
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).
* 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.
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.)
when cl_abap_typedescr=>typekind_class.
when cl_abap_typedescr=>typekind_intf.
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.
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.
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