There are only a few posts about CL_ABAP_TESTDOUBLE. I didn’t find it so trivial to use, even tricky sometimes. Even Google returns only 14 results for “CL_ABAP_TESTDOUBLE”. The most interesting information I could find is Prajul Meyana’s ABAP Test Double Framework – An Introduction, including a nice presentation video. This post is a very good starting point, but it would benefit from more examples.
So, I decided to create simple examples, for mocking methods with IMPORTING, EXPORTING, CHANGING and RETURNING parameters, class-based exceptions and events, and also the method TIMES.
Those examples are provided as a standalone demo program, containing an ABAP Unit class (all tests are passing, of course), in which you can check how test double call configurations are to be written, in case you have some doubts or issues with your own test doubles. I hope that those methods are enough self-documented. I tested this program in an ABAP 7.52 system.
Note that I don’t talk about “answers”, “matchers” and “expectations”, which allow dynamic decisions at runtime. They are explained in Prajul Meyana’s blog post.
Before showing the code, I tried to summarize a few key concepts that were not clear to me.
But, before going forward, please first read Prajul Meyana’s blog post, as it’s important that you first understand what the Test Double Framework is all about.
I hope this post will be useful.
Have fun with the ABAP Test Double Framework!
===> LATEST NEWS, March 16th, 2019: hey, what is the real point of using the ATD Framework, if you can simply create your own local class implementing the interface and code the rules you want… (discussion here: https://blogs.sap.com/2015/01/05/abap-test-double-framework-an-introduction/#comment-452240). By the way I moved the code to Github.
Reminder: limits of CL_ABAP_TESTDOUBLE
- Can mock interfaces only, not classes, and only global interfaces, not local ones.
- Can output exceptions of type “class-based” only (cannot mock those original old exceptions).
- A maximum of 36 interfaces can be test-doubled per test class, because the method CREATE of CL_ABAP_TESTDOUBLE generates one class at runtime by using a GENERATE SUBROUTINE POOL statement (generation of maximum 36 subroutine pools).
- Demonstrated in the test method “MAX_TEST_DOUBLES”
- Can’t mock static methods and events.
- Available from 7.40 SP 9. It seems that there’s no downport planned.
- Cannot mock parameters declared EXPORTING instead of CHANGING
- See below chapter “Parameters declared EXPORTING instead of CHANGING”
- Demonstrated in the test method “EXPORTING_BAD”
Reminder: call configuration = always 2 calls
What I didn’t clearly understand from Prajul Meyana’s post, was the obligatory sequence and presence of both the method CONFIGURE_CALL to define the output values, and the next method call on the test double to define the method name and input values, shortly said.
So, any call configuration must always (there’s no exception to this rule, never!) be made of these 2 consecutive calls, in this exact sequence:
- First call: define the output values or raise an exception, ignore some input parameters (to say any input value), and additionally raise events(s), etc.:
- CL_ABAP_TESTDOUBLE=>CONFIGURE_CALL( TEST_DOUBLE )-> … here chained methods …
- Example which uses almost all possibilities presented in this post:
cl_abap_testdouble=>configure_call( test_double )->ignore_parameter( 'IN1' )->ignore_parameter( 'IN2' )->returning( 40 )->set_parameter( name = 'OUT1' value = 38 )->set_parameter( name = 'OUT2' value = 'X' )->times( 999999999 )->raise_event( name = 'ACTION_DONE' parameters = VALUE #( ( name = 'PARAM_NAME' value = REF i( 20 ) ) ) )->raise_event( name = 'ACTION_DONE' parameters = VALUE #( ( name = 'PARAM_NAME' value = REF i( 40 ) ) ) ).
- Second call: choose the method and its input values (including the input values of changing parameters):
- TEST_DOUBLE->METHOD( input and changing parameters )
- The input values are only tested for equality by the framework, all other test operators are to be manually coded using “matchers” (not detailed in this post).
- There’s also another possibility, which is to define “any value” for one or more parameters. This must be done by:
- First, using the methods IGNORE_PARAMETER and IGNORE_ALL_PARAMETERS in the CONFIGURE_CALL above.
- Second, enter dummy values for all those “ignored” parameters in the second call. You may not enter those dummy values for those parameters which are optional).
- Don’t indicate parameters from the IMPORTING category, otherwise you’ll get an exception at runtime. Those parameter values must be passed via the SET_PARAMETER method in the first call.
If you want to mock more combinations of input and output values, like defining a decision table, or mocking several methods, you may define several call configurations, as many times as you want, on the same test double.
So, again, don’t forget, a call configuration is always composed of 2 calls: a call to CONFIGURE_CALL, followed by a call to the method to be configured.
Summary of the methods of the call configuration
Those methods belong to a so-called “fluent interface”, meaning that each instance method returns their own instance, so that you can chain the methods easily (cf the CONFIGURE_CALL example above).
- It sets the value of one parameter to be returned by the configured method; it’s valid only for exporting and changing parameters. For the returning parameter, you must use the RETURNING method. You must not mix SET_PARAMETER with RAISE_EXCEPTION in the call configuration of course.
- cf code of the test methods EXPORTING_GOOD and EXPORTING_BAD below.
- It sets the value for the returning parameter. You must not mix RETURNING with RAISE_EXCEPTION in the call configuration of course.
- cf code of the test method RETURNING_GOOD below.
- It triggers a class-based exception. You must not mix it with SET_PARAMETER and RETURNING in the call configuration, and only one RAISE_EXCEPTION can be used of course.
- cf code of the test methods RAISE_EXCEPTION_BAD and RAISE_EXCEPTION_GOOD below.
- It triggers an event (eventually with parameters) when the configured method is called; you may raise several events if you want to.
- cf code of the test method RAISE_EVENT_GOOD below.
- This method is used to implement a wild card “whatever the parameter value is” for a given parameter. You may call it several times, once per each parameter you want to “ignore”. It can be used for a Changing parameter.
- cf code of the test methods IGNORE_PARAMETER_BAD and IGNORE_PARAMETER_GOOD below.
- Same as IGNORE_PARAMETER, but it ignores all the importing and changing parameters.
- cf code of the test methods IGNORE_ALL_PARAMETERS_BAD and IGNORE_ALL_PARAMETERS_GOOD below.
- If not specified, the default is TIMES( 1 ), i.e. this call configuration is applied once.
- Important: if the configured method is called more times than the number indicated, the framework applies the last created call configuration for the method. This is demonstrated in the test methods EXPORTING_TWICE_GOOD and RETURNING_TWICE_GOOD below.
- This is very important to understand, especially when you define generic input values, as this kind of call configuration would probably be consumed many times, so you should use the method TIMES with a high-enough value. This is demonstrated in the test methods DECISION_TABLE_BAD* and DECISION_TABLE_GOOD below.
- For simple usages, cf code of the test methods TIMES_BAD and TIMES_GOOD below.
The following methods are not explained in this blog post, for more information cf Prajul Meyana’s blog post:
- In short, first create an “answer” class which returns some output values (or exceptions) based on the input values, of any method, and you pass an instance of it to SET_ANSWER.
- SET_MATCHER: (out of scope)
- In short, first create a “matcher” class which determines whether the call configuration applies or not based on the method and its input values, and you pass an instance of it to SET_MATCHER.
- AND_EXPECT: (out of scope)
- In short, it defines the expected number of calls of the configured method. The expectation is to be checked by calling the method VERIFY_EXPECTATIONS of CL_ABAP_TESTDOUBLE. Note: this method must always be used at the end of the chain, because it returns an instance which is not the call configuration.
Case study of an “else if” decision table with generic values, and why TIMES is so important
While doing the test methods DECISION_TABLE_BAD* and DECISION_TABLE_GOOD, I discovered how important the method TIMES is.
Those test methods test the method DECISION_TABLE, which is defined as follows:
METHODS decision_table IMPORTING p1 TYPE i p2 TYPE i RETURNING VALUE(result) TYPE i.
The call configurations defined in the test method DECISION_TABLE_GOOD corresponds to this decision table:
|Input P1||Input P2||Output RESULT|
|Any value||Any value||40|
Note that the sequence of the lines is important, as there is to be an “ELSE IF” rule, i.e. if P1 and P2 parameters have both values 5, all four lines of the decision table could apply, but using the ELSE IF rule, only the first line will be applied for those values and the output will be 10.
The call configurations corresponding to the decision table were written as follows, the order is as much important as in the decision table because the Test Double Framework chooses the “first” matching configuration (in the chronological order of their creation):
cl_abap_testdouble=>configure_call( test_double )->set_returning( 10 )->times( 999999 ). cut->decision_table( p1 = 5 p2 = 5 ). cl_abap_testdouble=>configure_call( test_double )->ignore_parameter( 'P1' )->set_returning( 20 )->times( 999999 ). cut->decision_table( p1 = 0 p2 = 5 ). cl_abap_testdouble=>configure_call( test_double )->ignore_parameter( 'P2' )->set_returning( 30 )->times( 999999 ). cut->decision_table( p1 = 5 p2 = 0 ). cl_abap_testdouble=>configure_call( test_double )->ignore_all_parameters( )->set_returning( 40 )->times( 999999 ). cut->decision_table( p1 = 0 p2 = 0 ).
Note that the convention I use here is to pass the generic parameter values (“ignored parameters”) with the value 0, although any value could be passed. Another value could be chosen if 0 was used as a real input value.
The method call TIMES( 999999 ) is very important:
If you don’t use TIMES or you use TIMES( 1 ) (that’s the same thing), you would get 30 instead of 10 if the method is called twice with the same input values, because the first call configuration would be invalid since it already occurred once. That’s demonstrated in the test methods DECISION_TABLE_*_BAD.
|Input P1||Input P2||Expected RESULT||Actual RESULT with TIMES( 1 )|
|5||5||10||30 (WRONG !)|
With TIMES( 999999 ), the results are correct (cf the test method DECISION_TABLE_GOOD):
|Input P1||Input P2||Expected RESULT||Actual RESULT with TIMES( 999999 )|
Parameters declared EXPORTING instead of CHANGING
Some incorrectly-defined methods use a parameter in the category EXPORTING instead of CHANGING. This design error usually remains unnoticed as, if this parameter is passed by reference, both categories work identically.
But for test doubles, with an EXPORTING parameter, if you want to return an output value depending on a given input value, then it’s impossible because an IMPORTING is forbidden by the framework.
METHODS cutmethod EXPORTING output TYPE i. METHOD cutmethod. output = output + 1. ENDMETHOD.
You’d like to mock it, but in fact you can’t, as it triggers an ATD exception:
cl_abap_testdouble=>configure_call( test_double )->set_parameter( name = 'OUTPUT' value = 11 ). data(output) = value i( 10 ). cut->cutmethod( IMPORTING output = output ). " <=== ATD exception at runtime
The only solutions I can think of, is to refactor the parameter to the category CHANGING, or if you can’t, to configure an “answer” instance (I don’t know whether it works, and I don’t discuss this topic in this post).
Code with the short examples
Finally, here is the code containing the Test Double Framework examples, wrapped into ABAP Unit classes.
It consists of 2 objects:
- Interface pool (SE24) ZIF_ATD_DEMO
- Executable program (SE38) Z_ATD_DEMO