Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
former_member230745
Discoverer
72,331
      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.

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

39 Comments
ceedee666
Active Contributor

Hi Prajul,

nice document.

In my opinion the test double framework should definitely be extended to also support global, non-final classes. There are many cases where no interfaces are available, especially in legacy code. Requiring an interface makes using the test double framework unnecessarily complex.

Best,

Christian

former_member230745
Discoverer
0 Kudos

Hi Christian,

Thanks.

Support for non final classes is already under discussion.

Best Regards,

Prajul

Former Member
0 Kudos

Very nice! Is there any specific reason why this is only available with 740 SP9 and higher or can this be downported by any chance?

Peter_Inotai
Active Contributor
0 Kudos

I guess it was also downported to 7.40 from some higher internal release (eg 7.50, 7.60 or 8.0x).

Anyway it would be nice to make it available for 7.30 and 7.20/7.02.

nomssi
Active Contributor
0 Kudos

Hello Prajul,

this framework will really simplify behavior verification.

I cannot give any feedback yet, but I have a question: would it be possible to extend the functionality to local classes and interfaces? Is there a technical limitation there?

Thanks for sharing.


best regards,

Jacques

denniss_
Explorer
0 Kudos

Nice and detailed, thanks Prajul.

And I agree to Christian, support for non-final classes is some kind of a must-have.

former_member182680
Active Participant
0 Kudos

Dear Peter,

I absolutely agree. It would be extremely helpful to have the test double framework for releases >= 7.02

In the meantime, you may want to have a look at mockA which runs starting from NW 7.01

https://github.com/uweku/mockA

Regards,

Uwe

Peter_Inotai
Active Contributor
0 Kudos

Hi Uwe,

Thanks for the info about mockA, I was not aware of it.

Cheers,

Peter

0 Kudos

Hi Jacques,

you are correct. Due to technical limitations the framework can't generate test doubles for local interfaces.

cheers

Thomas

0 Kudos

Hi Tapio,

we already did a downport to SAP BASIS 731 but it has not been officially released, yet. During the design and implementation phase of the framework we made sure that it can be downported to 7.02 as well. However, there are currently no plans for such a downport.

cheers

Thomas

Former Member
0 Kudos

I thought this is a core requirement for anything that generates mocks...
Relying on interfaces solely is really green-field :wink:

Former Member
0 Kudos

Hi Thomas,

sounds good. I believe there will be a lot of customers who will stay for lower Basis releases than 7.40 for a quite long time. So I think there is a great demand for a framework like this. Especially when you find out how useful TDD is...

Best regards, Tapio

OttoGold
Active Contributor
0 Kudos

:smile:

0 Kudos

Hi Otto,

eventually the effort to ensure this was not as high as you suspect , so stay calm and relax...

Regards,

Michael

Former Member
0 Kudos

Greate love it!

Former Member
0 Kudos

As far as I understand 'generate subroutine pool' is used each time for proxy object generation, however how the generation limit (maximum of 36 temporary subroutine pools) is revoked?

former_member182680
Active Participant
0 Kudos

Dear Iegor,

From what I've been seeing so far, the limit is there and applies also to the test double framework. However, subroutine pools are cached per interface, once the first proxy class has been generated. This means, per interface, you can have as many test doubles as you want, as the method outputs are not hard coded, but reseolved by a parameter handler internally. Only the second or third interface that you want test doubles being created for counts as number 2 or 3, until the subroutine limit is reached. This should certainly be enough 🙂

majcon
Active Participant

Hi Prajul,

really nice introduction! I like it a lot 😉

Is there any official SAP Help Documentation planned regarding describing this topic official like for example the ABAP Unit Topics?

By the way, do you know when (or better if) the suggested creation of the testdouble over an global (why not also for local context...) class is making any progress?

Thanks in advance for an reply.

Best regards,

Damir

SuhaSaha
Advisor
Advisor
0 Kudos

Hi Prajul,

I am on ABAP Release 740 SP 12, but the package SABP_UNIT_TEST_DOUBLE_CORE is still marked as "strictly internal" :???:

Does the framework (still) support only global interfaces?

BR,

Suhas

Peter_Inotai
Active Contributor
0 Kudos

It's actually "Strictely Internal" :smile:

0 Kudos

Hi,

good catch! Sorry, we forgot to update the description. This has been fixed in the newer releases. There is actually a simple way for checking whether it is supported or not. See if package interface: SABP_UNIT_TD_API is available. It defines all interfaces/classes which are released for consumption.

cheers

Thomas

SuhaSaha
Advisor
Advisor
0 Kudos

Hi Thomas,

The package interface is available :wink:

Now back to the burning question,


Does the test double framework support (non-final) global classes?


I can see that the interface IF_ATD_PROXY_GENERATOR is implemented by -

  • CL_ATD_PROXY_GENERATOR_CLASS
  • CL_ATD_PROXY_GENERATOR_INTF

But the factory method CL_ATD_PROXY_FACTORY=>CREATE_PROXY( ) supports only interfaces :shock:

Bummer! Or am i missing something?

BR,

Suhas

0 Kudos

Hi Suhas,

support for classes is still in our backlog. It will be implemented in one of the next releases.

cheers

Thomas

Former Member
0 Kudos

This looks very nice and it was about time SAP introduced their own mocking framework. As others mentioned a downport would be great (along with support for classes, not just interfaces).

hardyp180
Active Contributor

In regard to mocking an exception, from my testing it looks like the exception has to be declared in the signature of the method being mocked?

Some classes raise a "NO_CHECK" exception (for better or worse) or sometimes a DYNAMIC_CHECK which is not mentioned in the signature (definitely for the worse). I gather such behaviour cannot be mocked using the ATDF?

Downporting to 7.02 would be wonderful, I will be on that release until 2025, in regard to classes as well as interfaces I am not so fussed, because one of the greatest computer programmers of all time, Joe Dolce, raised the very valid point that if your class does not implement any interfaces then it has gotta no respect.

Cheersy Cheers

Paul

Former Member
0 Kudos
I have the same problem. The framework throws an error ( "The configured exception ... is not declared in method ...") when I want it to raise an unchecked exception.
Maybe the devs forgot about NO_CHECK? I hope this will be fixed soon, like this it is not possible to verify my error handling with the ATDF, I have to use mockA for that.

 
lukasbartek
Explorer
0 Kudos

Hi Prajul,

Are there any plans to support test doubles for local interfaces?

You have mentioned that this is not possible because of technical limitations. ABAP 7.50 introduce Test Seams to break out dependencies that would otherwise prevent writing an unit tests. It’s great to see that SAP is more serious about ABAP Unit Tests and introduced constructs to deal better with legacy code. I think it would be really good if we had full support of test doubles for global and local classes.

Best regards,
Lukas

xczar0
Explorer
0 Kudos
Hello Prajul!

Nice you created such extensive description!

I've got a question as i cannot make "returning" values for test doubles working.

First thing i thought about was - "I'm doing something wrong, i'll better copy your coding and analyze where i did a mistake".

So i copied your interface entirely, made it global not local. And used your test class with small modifications:
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 zif_ck_td_currency_converter.

lo_currency_converter_double ?= cl_abap_testdouble=>create( 'zif_ck_td_currency_converter' ).
cl_abap_testdouble=>configure_call( lo_currency_converter_double )->returning( 80 ).
data(val80) = lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = 'USD'
target_currency = 'EUR'
).
cl_abap_unit_assert=>assert_equals( exp = 80 act = val80 ).
ENDMETHOD.

ENDCLASS.

Here i expected the test to be successful as method should return 80.

Yet it didn't return anything for some strange reason:



So i decided to debug process of assignment of values.

First thing - it's created correctly:



So maybe i didn't understand the process or returning the value out of test double? I decided to check:

I noticed this param is being set even though i did it in the configuration in the first place.



So as i_value is not passed to the method (as this is a returning paramter) it's later on being set to initial value  = 0 in "set_parameter" call.



I probably didn't understand it correctly - could you maybe point where i the mistake so it cannot work for me?

Cheers,

Cezary
Sandra_Rossi
Active Contributor
0 Kudos
cezaryk , you must always do a double call to define the behavior:

  1. First do the configuration call to define the output parameters

  2. Then call the method to define the input parameters


After that, you may call the method a second time, which will apply the defined behavior.

In your case, you called the method only once.

You should do:
* define the expected output
cl_abap_testdouble=>configure_call( lo_currency_converter_double )->returning( 80 ).
* define the input parameters, i.e. the condition whom expected output applies
* (note that it doesn't make sense to read the returned value, as none will be returned here)
lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = 'USD'
target_currency = 'EUR'
).
* now do the "actual" call, which calls the test double
data(val80) = lo_currency_converter_double->convert(
EXPORTING
amount = 100
source_currency = 'USD'
target_currency = 'EUR'
).

 
former_member525226
Discoverer
0 Kudos
Same problem here.

With the general understanding of the best practices of using exceptions heavily shifting towards using checked exceptions only in a few well defined cases (that's happening even in Java world, which introduced checked exceptions in the first place) this looks like a big issue. Possibly even a show-stopper.
EnnoWulff
Active Contributor
Thanks prajul.meyana for clarifying the use of SAP's test doubles framework.

Yes, I know, the post is older than four years now, but I think that this approach hasn't changed at all?!

IMHO the cl_abap_testdouble is totally nonsense!

As christian.drumm already mentioned: As long es I need an interface, it's so much easier to simply create a new class where I can hard code the desired values. If I am at the point where I can use an interface, because the application thankfully supports dependency injection for the right methods, then it is easier, less complicated, clearer and uses less code to implement a test-double-class for my test cases.
"mock definition
CLASS lcl_currency_converter_mock_80 DEFINITION.
PUBLIC SECTION.
INTERFACES if_td_currency_converter.
ENDCLASS.

CLASS lcl_currency_converter_mock_80 IMPLEMENTATION.
METHOD if_td_currency_converter~convert.
result = 80.
ENDMETHOD.
ENDCLASS.

"injecting the test double into the object being tested
CREATE OBJECT lo_expense_manager
EXPORTING
currency_converter = NEW lcl_currency_converter_mock_80( ).

 

Or do I miss an important point?

Thanks
~Enno
Sandra_Rossi
Active Contributor
... and better performance and unlimited use because the method CREATE of CL_ABAP_TEST_DOUBLE does a GENERATE SUBROUTINE POOL to create a local interface at run time, so max 36 CREATE per test class.
JonAmos
Explorer
0 Kudos
After reading https://martinfowler.com/articles/mocksArentStubs.html, it looks like this is a "stub" and cl_abap_testdouble is used for the "mock" approach.
0 Kudos
Thanks for this Blog. It helped me to create faster Abap Units for some methods, but I also miss some features and are curious whether SAP still develops this framework.

For example, I will be happy if I can get some more information about the interaction with the methods, like a diff between the actual calls and the expected calls.
hardyp180
Active Contributor
I have been creating my own test double classes for the reasons you state. The whole ABAP Test Double Framework seems a touch on the over-complicated side. It is said that creating test double classes by hand is "tedious" but the ATDF seems a lot more tedious to me!

Most importantly saying what the expected result is first, before saying what method is going to be doubled is bonkers, unless I am also missing something obvious.

And what if the method being tested has two calls to the test double object using different methods each time?

Cheersy Cheers

Paul

 
hardyp180
Active Contributor
And I just noticed that if in Eclipse I try to create a test double of a local class that does not yet exist yet starts with LTD_ then the quick fix is clever enough to work out I want to create a test double class and does the definition and implementation all for me. It is not clever enough yet to work out that because I have typed the variable I am trying to create based on an interface, that the test double should implement that interface, but that is trivial.

Ten seconds later I have (blank) implementations for all the interface methods. How in the world can this be considered tedious? It would be tedious in SE80 - of that there is no doubt!
merveguel
Participant
al_01
Explorer
0 Kudos
One benefit might be that you do not have to implement the interface various times if you want to test the same method in different scenarios (with different input/output/exceptions...).

Writing own test doubles for interfaces with more than one method might end up in different implementations with most of the implemented methods being empty... (Unless using an abstract base class or inheritence ...).
0 Kudos
Thanks for this Blog.
Labels in this area