Dependent objects which can’t 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.
Let’s 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. It’s primarily used by configuring the output values. The second statement is a call on the double object. It’s 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 won’t 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.
No comments:
Post a Comment