Wednesday, 4 April 2018

ABAP Test Double Framework – An Introduction

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