ABAP Test Double Framework – An Introduction
What is it
Quick Demo Video
Getting started
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.
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.
Configuring outputs for method calls
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.
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
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.
Configuring events for method calls
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.
Changing method call behavior based upon the 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.
Verifying Interactions
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.
Advanced Topics
Implementing Custom Matchers
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
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.
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
Hi Christian,
Thanks.
Support for non final classes is already under discussion.
Best Regards,
Prajul
I thought this is a core requirement for anything that generates mocks...
Relying on interfaces solely is really green-field 😉
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?
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.
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
Hi Uwe,
Thanks for the info about mockA, I was not aware of it.
Cheers,
Peter
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
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
🙂
Hi Otto,
eventually the effort to ensure this was not as high as you suspect , so stay calm and relax...
Regards,
Michael
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
Hi Jacques,
you are correct. Due to technical limitations the framework can't generate test doubles for local interfaces.
cheers
Thomas
Nice and detailed, thanks Prajul.
And I agree to Christian, support for non-final classes is some kind of a must-have.
Greate love it!
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?
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 🙂
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
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
It's actually "Strictely Internal" 🙂
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
Hi Thomas,
The package interface is available 😉
Now back to the burning question,
I can see that the interface IF_ATD_PROXY_GENERATOR is implemented by -
But the factory method CL_ATD_PROXY_FACTORY=>CREATE_PROXY( ) supports only interfaces 😯
Bummer! Or am i missing something?
BR,
Suhas
Hi Suhas,
support for classes is still in our backlog. It will be implemented in one of the next releases.
cheers
Thomas
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).
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
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.
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.
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
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:
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
Former Member , you must always do a double call to define the behavior:
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:
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.
Or do I miss an important point?
Thanks
~Enno
... 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.
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.
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
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!
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 ...).
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.
Kindly asking your help on my issue..
https://answers.sap.com/questions/13113270/test-double-framework-how-to-bypass-the-function-c.html