Monday 28 May 2018

Short examples of CL_ABAP_TESTDOUBLE

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 method, 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).
  • Can’t mock static methods and events (of course).
  • Available from 7.40 SP 9. It seems that there’s no downport planned.
  • Cannot mock parameters declared EXPORTING instead of CHANGING.

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.

SAP ABAP Tutorials and Materials, SAP ABAP Learning, SAP ABAP Certifications

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).
  • SET_PARAMETER:
    • 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.
  • RETURNING:
    • 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.
  • RAISE_EXCEPTION:
    • 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.
  • RAISE_EVENT:
    • 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.
  • IGNORE_PARAMETER:
    • 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.
  • IGNORE_ALL_PARAMETERS
    • 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.
  • TIMES:
    • 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.
  • SET_ANSWER:
    • It’s not explained here, for more information cf Prajul Meyana’s blog post. In short, first create an “answer” class which defines the output values (or exception), based on the input values, of any method, and you pass an instance of it to SET_ANSWER.
  • SET_MATCHER:
    • It’s not explained here, for more information cf Prajul Meyana’s blog post. In short, first create a “matcher” class which determines whether the call configuration applies or not based on the method and input values, and you pass an instance of it to SET_ANSWER.
  • AND_EXPECT:
    • It’s not explained here, for more information cf Prajul Meyana’s blog post. 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 
5 10 
Any value 5 20 
5 Any value  30 
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 10  10
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( 1 )
66 66 40 40
66 66 40 40
5 66 30 30
5 66 30 30
66 5 20 20
66 5 20 20
5 5 10 10
5 5 10 10

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.

Example:

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:

First, one interface pool (SE24), which is required for using the test double framework:

INTERFACE zif_atd_demo
  PUBLIC .
  EVENTS action_done
    EXPORTING
      VALUE(param_name) TYPE i .
  METHODS demo_raise_event .
  METHODS several_cases
    IMPORTING
      !whatever     TYPE i
    RETURNING
      VALUE(result) TYPE i .
  METHODS decision_table
    IMPORTING
      !p1           TYPE i
      !p2           TYPE i
    RETURNING
      VALUE(result) TYPE i .
  METHODS demo_raise_exception.
  METHODS demo_raise_exception2
    IMPORTING
      !whatever TYPE i OPTIONAL
    RETURNING
      VALUE(result) TYPE i .
  METHODS exporting
    EXPORTING
      !result TYPE i .
  METHODS importing_required
    IMPORTING
      !whatever     TYPE i
    RETURNING
      VALUE(result) TYPE i .
  METHODS importing_optional
    IMPORTING
      !whatever     TYPE i DEFAULT 33
    RETURNING
      VALUE(result) TYPE i .
  METHODS demo_returning
    RETURNING
      VALUE(result) TYPE i .
  METHODS exporting_returning
    EXPORTING
      !exporting       TYPE i
    RETURNING
      VALUE(returning) TYPE i .
  METHODS changing
    CHANGING
      !whatever TYPE i .
ENDINTERFACE.
Second, the report (SE38) with ABAP Unit classes:

REPORT.
CLASS lcl_cut DEFINITION
  FINAL
  CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES zif_atd_demo.
    METHODS constructor
      IMPORTING
        atd_aunit TYPE REF TO zif_atd_demo.
  PRIVATE SECTION.
    DATA: mr_atd_aunit TYPE REF TO zif_atd_demo.
ENDCLASS.
CLASS lcx_cut DEFINITION INHERITING FROM cx_no_check.
ENDCLASS.
CLASS ltc_atd_aunit DEFINITION
  INHERITING FROM cl_aunit_assert
  FINAL
  FOR TESTING
DURATION SHORT
  RISK LEVEL HARMLESS.
  PRIVATE SECTION.
    METHODS:
      setup,
      raise_exception_good FOR TESTING RAISING cx_static_check,
      raise_exception_good2 FOR TESTING RAISING cx_static_check,
      raise_exception_bad FOR TESTING RAISING cx_static_check,
      exporting_good FOR TESTING RAISING cx_static_check,
      exporting_bad FOR TESTING RAISING cx_static_check,
      exporting_returning_good FOR TESTING RAISING cx_static_check,
      returning_good FOR TESTING RAISING cx_static_check,
      exporting_twice_good FOR TESTING RAISING cx_static_check,
      returning_twice_good FOR TESTING RAISING cx_static_check,
      times_bad FOR TESTING RAISING cx_static_check,
      times_zero_bad FOR TESTING RAISING cx_static_check,
      times_good FOR TESTING RAISING cx_static_check,
      times_good2 FOR TESTING RAISING cx_static_check,
      importing_required_bad FOR TESTING RAISING cx_static_check,
      importing_required_good FOR TESTING RAISING cx_static_check,
      importing_required_good2 FOR TESTING RAISING cx_static_check,
      importing_optional_bad FOR TESTING RAISING cx_static_check,
      importing_optional_good FOR TESTING RAISING cx_static_check,
      importing_optional_good2 FOR TESTING RAISING cx_static_check,
      importing_optional_good3 FOR TESTING RAISING cx_static_check,
      changing_good FOR TESTING RAISING cx_static_check,
      ignore_parameter_bad FOR TESTING RAISING cx_static_check,
      ignore_parameter_good FOR TESTING RAISING cx_static_check,
      ignore_all_parameters_bad FOR TESTING RAISING cx_static_check,
      ignore_all_parameters_good FOR TESTING RAISING cx_static_check,
      two_methods_bad FOR TESTING RAISING cx_static_check,
      two_methods_good FOR TESTING RAISING cx_static_check,
      several_cases_good FOR TESTING RAISING cx_static_check,
      decision_table_good FOR TESTING RAISING cx_static_check,
      decision_table_bad FOR TESTING RAISING cx_static_check,
      decision_table_bad2 FOR TESTING RAISING cx_static_check,
      decision_table_bad3 FOR TESTING RAISING cx_static_check,
      decision_table_bad4 FOR TESTING RAISING cx_static_check,
      order_of_calls_bad FOR TESTING RAISING cx_static_check,
      order_of_calls_bad2 FOR TESTING RAISING cx_static_check,
      raise_event_good FOR TESTING RAISING cx_static_check,
      max_test_doubles FOR TESTING RAISING cx_static_check.
    METHODS on_action_done FOR EVENT action_done OF zif_atd_demo IMPORTING param_name.
    METHODS decision_table_init_bad.
    DATA: test_double TYPE REF TO zif_atd_demo,
          cut         TYPE REF TO lcl_cut,
          result      TYPE i.
    DATA event_param_value TYPE string.
ENDCLASS.
CLASS lcl_cut IMPLEMENTATION.
  METHOD constructor.
    mr_atd_aunit = atd_aunit.
  ENDMETHOD.
  METHOD zif_atd_demo~exporting.
    mr_atd_aunit->exporting(
      IMPORTING
        result = result
    ).
  ENDMETHOD.
  METHOD zif_atd_demo~demo_raise_exception.
    mr_atd_aunit->demo_raise_exception( ).
  ENDMETHOD.
  METHOD zif_atd_demo~demo_raise_exception2.
    result = mr_atd_aunit->demo_raise_exception2( whatever ).
  ENDMETHOD.
  METHOD zif_atd_demo~demo_returning.
    result = mr_atd_aunit->demo_returning( ).
  ENDMETHOD.
  METHOD zif_atd_demo~importing_optional.
    IF whatever IS NOT SUPPLIED.
      result = mr_atd_aunit->importing_optional( ).
    ELSE.
      result = mr_atd_aunit->importing_optional( whatever ).
    ENDIF.
  ENDMETHOD.
  METHOD zif_atd_demo~importing_required.
    result = mr_atd_aunit->importing_required( whatever ).
  ENDMETHOD.
  METHOD zif_atd_demo~changing.
    mr_atd_aunit->changing( CHANGING whatever = whatever ).
  ENDMETHOD.
  METHOD zif_atd_demo~demo_raise_event.
    mr_atd_aunit->demo_raise_event( ).
  ENDMETHOD.
  METHOD zif_atd_demo~several_cases.
    result = mr_atd_aunit->several_cases( whatever = whatever ).
  ENDMETHOD.
  METHOD zif_atd_demo~decision_table.
    result = mr_atd_aunit->decision_table( p1 = p1 p2 = p2 ).
  ENDMETHOD.
  METHOD zif_atd_demo~exporting_returning.
    returning = mr_atd_aunit->exporting_returning( IMPORTING exporting = exporting ).
  ENDMETHOD.
ENDCLASS.
CLASS ltc_atd_aunit IMPLEMENTATION.
  METHOD setup.
    " create test double object
    test_double ?= cl_abap_testdouble=>create( 'zif_atd_demo' ).
    " injecting the test double into the object to be tested (Code Under Test)
    CREATE OBJECT cut EXPORTING atd_aunit = test_double.
  ENDMETHOD.
  METHOD raise_exception_good.
    " GOOD EXAMPLE
    " When the method DEMO_RAISE_EXCEPTION is called
    " Then an exception should be raised
    DATA: lx_exp TYPE REF TO lcx_cut,
          lx_act TYPE REF TO lcx_cut.
    CREATE OBJECT lx_exp.
    cl_abap_testdouble=>configure_call( test_double )->raise_exception( lx_exp ).
    test_double->demo_raise_exception( ).
    TRY.
        cut->zif_atd_demo~demo_raise_exception( ).
      CATCH lcx_cut INTO lx_act.
    ENDTRY.
    assert_equals( exp = lx_exp act = lx_act ).
  ENDMETHOD.
  METHOD raise_exception_good2.
    " GOOD EXAMPLE
    " When the method DEMO_RAISE_EXCEPTION2 is called with parameter 20
    " Then an exception should be raised
    "
    " When the method DEMO_RAISE_EXCEPTION2 is called with parameter 30
    " Then an exception should not be raised and the returned value should be 88
    DATA: lx_exp TYPE REF TO lcx_cut,
          lx_act TYPE REF TO lcx_cut.
    CREATE OBJECT lx_exp.
    cl_abap_testdouble=>configure_call( test_double )->raise_exception( lx_exp ).
    test_double->demo_raise_exception2( 20 ).
    cl_abap_testdouble=>configure_call( test_double )->returning( 88 ).
    test_double->demo_raise_exception2( 30 ).
    TRY.
        cut->zif_atd_demo~demo_raise_exception2( 20 ).
      CATCH lcx_cut INTO lx_act.
    ENDTRY.
    assert_equals( exp = lx_exp act = lx_act ).
    result = cut->zif_atd_demo~demo_raise_exception2( 30 ).
    assert_equals( exp = 88 act = result ).
  ENDMETHOD.
  METHOD raise_exception_bad.
    " BAD EXAMPLE
    " When the call configuration uses both RETURNING and RAISE_EXCEPTION
    " Then an exception '[ ABAP Testdouble Framework ] Illegal combination
    "       of RETURNING with RAISE_EXCEPTION' should be raised
    "
    " When the call configuration uses both SET_PARAMETER and RAISE_EXCEPTION
    " Then an exception 'Illegal config call. Previous
    "       configuration not complete' should be raised
    DATA: lx_exp TYPE REF TO lcx_cut,
          lx2 TYPE REF TO cx_atd_exception.
    CREATE OBJECT lx_exp.
    TRY.
        cl_abap_testdouble=>configure_call( test_double )->raise_exception( lx_exp )->returning( 90 ).
        fail( ).
      CATCH cx_atd_exception INTO lx2.
        assert_equals( exp = cx_atd_exception=>illegal_combi_exception
                       act = lx2->if_t100_message~t100key ).
    ENDTRY.
    TRY.
        cl_abap_testdouble=>configure_call( test_double )->raise_exception( lx_exp )->set_parameter( name = 'OUTPUT' value = 90 ).
        fail( ).
      CATCH cx_atd_exception INTO lx2.
        assert_equals( exp = cx_atd_exception=>illegal_config_call
                       act = lx2->if_t100_message~t100key ).
    ENDTRY.
  ENDMETHOD.
  METHOD exporting_bad.
    " BAD EXAMPLE
    " When the call configuration contains IMPORTING
    " Then an ABAP Test Double exception should be raised
    " Solution: you retrieve the results via SET_PARAMETER in the CONFIGURE_CALL
    DATA lx2 TYPE REF TO cx_atd_exception.
    cl_abap_testdouble=>configure_call( test_double )->set_parameter( name = 'RESULT' value = 20 ).
    TRY.
        test_double->exporting( IMPORTING result = result ).
      CATCH cx_atd_exception INTO lx2.
    ENDTRY.
    assert_bound( lx2 ).
    assert_equals( exp = cx_atd_exception=>export_not_allowed
                   act = lx2->if_t100_message~t100key ).
  ENDMETHOD.
  METHOD exporting_good.
    " GOOD EXAMPLE
    " When the method EXPORTING is called
    " Then it should return the exporting parameter RESULT with value 20
    cl_abap_testdouble=>configure_call( test_double )->set_parameter( name = 'RESULT' value = 20 ).
    test_double->exporting( ).
    cut->zif_atd_demo~exporting( IMPORTING result = result ).
    assert_equals( exp = 20 act = result ).
  ENDMETHOD.
  METHOD returning_good.
    " GOOD EXAMPLE
    " When the method DEMO_RETURNING is called
    " Then it should return the returning parameter RESULT with value 97
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    test_double->demo_returning( ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD exporting_twice_good.
    " GOOD EXAMPLE
    " When the method EXPORTING is called multiple times
    " Then it should always return the exporting parameter RESULT with value 20
    cl_abap_testdouble=>configure_call( test_double )->set_parameter( name = 'RESULT' value = 20 ).
    test_double->exporting( ).
    cut->zif_atd_demo~exporting( IMPORTING result = result ).
    cut->zif_atd_demo~exporting( IMPORTING result = result ).
    assert_equals( exp = 20 act = result ).
  ENDMETHOD.
  METHOD returning_twice_good.
    " GOOD EXAMPLE
    " When the method DEMO_RETURNING is called multiple times
    " Then it should return the returning parameter RESULT with value 97
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    test_double->demo_returning( ).
    result = cut->zif_atd_demo~demo_returning( ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD times_bad.
    " BAD EXAMPLE
    " When the method DEMO_RETURNING is called the first time
    " Then it should return the returning parameter RESULT with value 97
    "
    " When the method DEMO_RETURNING is called the second time
    " Then it should return the returning parameter RESULT with value 0
    " But it returns 97 !
    " Solution: you should define another call configuration, because
    " the last one is always returned.
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 97
          )->times( 1 ). " useless, times( 1 ) is the default if not specified
    test_double->demo_returning( ).
    result = cut->zif_atd_demo~demo_returning( ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD times_good.
    " GOOD EXAMPLE
    " When the method DEMO_RETURNING is called the first time
    " Then it should return the returning parameter RESULT with value 97
    "
    " When the method DEMO_RETURNING is called the next times
    " Then it should return the returning parameter RESULT with value 0
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 97 ).
    test_double->demo_returning( ).
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 0 ).
    test_double->demo_returning( ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 97 act = result ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 0 act = result ).
    " from now on (max times consumed), the last configuration is used
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD times_good2.
    " GOOD EXAMPLE
    " When the method DEMO_RETURNING is called the two first times
    " Then it should return the returning parameter RESULT with value 97
    "
    " When the method DEMO_RETURNING is called the next times
    " Then it should return the returning parameter RESULT with value 0
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 97
          )->times( 2 ).
    test_double->demo_returning( ).
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 0 ).
    test_double->demo_returning( ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 97 act = result ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 97 act = result ).
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 0 act = result ).
    " from now on (max times consumed), the last configuration is used
    result = cut->zif_atd_demo~demo_returning( ).
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD times_zero_bad.
    " TIMES( 0 ) doesn't mean unlimited number of times
    DATA lx2 TYPE REF TO cx_atd_exception.
    TRY.
        cl_abap_testdouble=>configure_call( test_double
              )->returning( 97
              )->times( 0 ). " <--- INVALID
      CATCH cx_atd_exception INTO lx2.
    ENDTRY.
    assert_bound( lx2 ).
    assert_equals( exp = cx_atd_exception=>times_zero
                   act = lx2->if_t100_message~t100key ).
  ENDMETHOD.
  METHOD exporting_returning_good.
    " GOOD EXAMPLE
    " When the method EXPORTING_RETURNING is called
    " Then it should return the returning parameter with value 97
    "  and the exporting parameter with value 135
    DATA: exporting TYPE i.
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 97
          )->set_parameter( name = 'EXPORTING' value = 135 ).
    test_double->exporting_returning( ).
    result = cut->zif_atd_demo~exporting_returning( IMPORTING exporting = exporting ).
    assert_equals( exp = 97 act = result ).
    assert_equals( exp = 135 act = exporting ).
  ENDMETHOD.
  METHOD importing_required_bad.
    " BAD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with parameter 5555
    " Then it should return 97
    " But it returns 0 !
    " Solution: replace 5554 with 5555 in the call configuration
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    test_double->importing_required( 5554 ).
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD importing_required_good.
    " GOOD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with parameter 5555
    " Then it should return 97
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    test_double->importing_required( 5555 ).
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD importing_required_good2.
    " GOOD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with any parameter value
    " Then it should return 97
    " (note: this test method is the duplicate of IGNORE_PARAMETER_GOOD)
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'WHATEVER' )->returning( 97 ).
    test_double->importing_required( whatever = 5554 ). " any value, it's ignored
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD importing_optional_bad.
    " BAD EXAMPLE
    " When the method IMPORTING_OPTIONAL is called with the optional parameter equal to any value,
    " Then it should return 97
    " But it returns 0 !
    " That doesn't work because you should use the method IGNORE_PARAMETER to indicate "any value".
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    test_double->importing_optional( ).
    result = cut->zif_atd_demo~importing_optional( 5555 ).
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD importing_optional_good.
    " GOOD EXAMPLE
    " When the method IMPORTING_OPTIONAL is called with the optional parameter equal to any value,
    " Then it should return 97
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'WHATEVER' )->returning( 97 ).
    test_double->importing_optional( ).
    result = cut->zif_atd_demo~importing_optional( whatever = 5555 ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD importing_optional_good2.
    " GOOD EXAMPLE
    " When the method IMPORTING_OPTIONAL is called without the optional parameter
    " Then it should return 97
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    test_double->importing_optional( ).
    result = cut->zif_atd_demo~importing_optional( ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD importing_optional_good3.
    " GOOD EXAMPLE
    " When the method IMPORTING_OPTIONAL is called with the optional parameter 5555
    " Then it should return 97
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    test_double->importing_optional( 5555 ).
    result = cut->zif_atd_demo~importing_optional( 5555 ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD changing_good.
    " GOOD EXAMPLE
    " When the method CHANGING is called with the changing parameter equal to 0
    " Then it should be changed to 40
    "
    " When the method CHANGING is called with the changing parameter equal to 50
    " Then it should be changed to 110
    DATA: whatever TYPE i.
    cl_abap_testdouble=>configure_call( test_double
          )->set_parameter( name = 'WHATEVER' value = 40 ).
    whatever = 0.
    test_double->changing( CHANGING whatever = whatever ).
    cl_abap_testdouble=>configure_call( test_double
          )->set_parameter( name = 'WHATEVER' value = 110 ).
    whatever = 50.
    test_double->changing( CHANGING whatever = whatever ).
    whatever = 0.
    cut->zif_atd_demo~changing( CHANGING whatever = whatever ).
    whatever = whatever + 10.
    cut->zif_atd_demo~changing( CHANGING whatever = whatever ).
    assert_equals( exp = 110 act = whatever ).
  ENDMETHOD.
  METHOD ignore_parameter_bad.
    " BAD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with any parameter value
    " Then it should return 97
    " But it returns 0 !
    " Solution: you missed the second part of the call configuration!
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'WHATEVER' )->returning( 97 ).
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD ignore_parameter_good.
    " GOOD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with any parameter value
    " Then it should return 97
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'WHATEVER' )->returning( 97 ).
    test_double->importing_required( 1234 ). " <=== important call & dummy value !
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD ignore_all_parameters_bad.
    " BAD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with any parameter value
    " Then it should return 97
    " But it returns 0 !
    " Solution: you missed the second part of the call configuration!
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_all_parameters( )->returning( 97 ).
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD ignore_all_parameters_good.
    " GOOD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with any parameter value
    " Then it should return 97
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_all_parameters( )->returning( 97 ).
    test_double->importing_required( 5554 ). " (dummy value)
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 97 act = result ).
  ENDMETHOD.
  METHOD two_methods_bad.
    " BAD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with any parameter value
    " Then it should return 97
    "
    " When the method DEMO_RETURNING is called
    " Then it should return 99
    " But it returns 0 !
    " Solution: you missed the first part of the call configuration! (to set 99)
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_all_parameters( )->returning( 97 ).
    test_double->importing_required( 17 ). " (dummy value)
    test_double->demo_returning( ).
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 97 act = result ).
    result = test_double->demo_returning( ).
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD two_methods_good.
    " GOOD EXAMPLE
    " When the method IMPORTING_REQUIRED is called with any parameter value
    " Then it should return 97
    "
    " When the method DEMO_RETURNING is called
    " Then it should return 99
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_all_parameters( )->returning( 97 ).
    test_double->importing_required( 17 ). " (dummy value)
    cl_abap_testdouble=>configure_call( test_double )->returning( 99 ).
    test_double->demo_returning( ).
    result = cut->zif_atd_demo~importing_required( 5555 ).
    assert_equals( exp = 97 act = result ).
    result = test_double->demo_returning( ).
    assert_equals( exp = 99 act = result ).
  ENDMETHOD.
  METHOD several_cases_good.
    " GOOD EXAMPLE
    " When the method SEVERAL_CASES is called with value 27
    " Then it should return 40
    "
    " When the method SEVERAL_CASES is called with value 29
    " Then it should return 70
    cl_abap_testdouble=>configure_call( test_double )->returning( 40 ).
    test_double->several_cases( 27 ).
    cl_abap_testdouble=>configure_call( test_double )->returning( 70 ).
    test_double->several_cases( 29 ).
    result = cut->zif_atd_demo~several_cases( 27 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~several_cases( 29 ).
    assert_equals( exp = 70 act = result ).
    result = cut->zif_atd_demo~several_cases( 27 ).
    assert_equals( exp = 40 act = result ).
  ENDMETHOD.
  METHOD decision_table_good.
    " case which returns 10
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 10 )->times( 999999999 ).
    test_double->decision_table( p1 = 5 p2 = 5 ).
    " case which returns 20
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'P1' )->returning( 20 )->times( 999999999 ).
    test_double->decision_table( p1 = 0 p2 = 5 ).
    " case which returns 30
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'P2' )->returning( 30 )->times( 999999999 ).
    test_double->decision_table( p1 = 5 p2 = 0 ).
    " case which returns 40
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'P1' )->ignore_parameter( 'P2'
          )->returning( 40 )->times( 999999999 ).
    test_double->decision_table( p1 = 0 p2 = 0 ).
    " When there's only one matching case
    " Then the result is trivial
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 66 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 66 ).
    assert_equals( exp = 40 act = result ).
    " Scenario Outline
    " When there are several matching cases
    " Then all of them are returned in turn
    " Examples :
    " 1) The possible cases are 30 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 30 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 30 act = result ).
    " 2) The possible cases are 20 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 20 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 20 act = result ).
    " 3) The possible cases are 10, 20, 30 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 10 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 10 act = result ).
  ENDMETHOD.
  METHOD decision_table_bad.
    decision_table_init_bad( ).
    " When there's only one matching case
    " Then the result is trivial
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 66 ).
    assert_equals( exp = 40 act = result ).
    " Scenario Outline
    " When there are several matching cases
    " Then all of them are returned in turn
    " Examples :
    " 1) The possible cases are 30 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 30 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 40 act = result ).
  ENDMETHOD.
  METHOD decision_table_bad2.
    decision_table_init_bad( ).
    " Scenario Outline
    " When there are several matching cases
    " Then all of them are returned in turn
    " Examples :
    " 2) The possible cases are 20 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 20 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
  ENDMETHOD.
  METHOD decision_table_bad3.
    decision_table_init_bad( ).
    " Scenario Outline
    " When there are several matching cases
    " Then all of them are returned in turn
    " Examples :
    " 3) The possible cases are 10, 20, 30 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 10 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 20 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 30 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
  ENDMETHOD.
  METHOD decision_table_bad4.
    decision_table_init_bad( ).
    " Scenario Outline
    " When there are several matching cases
    " Then all of them are returned in turn
    " Examples :
    " 1) The possible cases are 30 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 30 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 66 ).
    assert_equals( exp = 40 act = result ).
    " 2) The possible cases are 20 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 20 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 66 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
    " 3) The possible cases are 10, 20, 30 and 40:
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 10 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
    result = cut->zif_atd_demo~decision_table( p1 = 5 p2 = 5 ).
    assert_equals( exp = 40 act = result ).
  ENDMETHOD.
  METHOD decision_table_init_bad.
    " case which returns 10
    cl_abap_testdouble=>configure_call( test_double
          )->returning( 10 ).
    test_double->decision_table( p1 = 5 p2 = 5 ).
    " case which returns 20
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'P1' )->returning( 20 ).
    test_double->decision_table( p1 = 0 p2 = 5 ).
    " case which returns 30
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'P2' )->returning( 30 ).
    test_double->decision_table( p1 = 5 p2 = 0 ).
    " case which returns 40
    cl_abap_testdouble=>configure_call( test_double
          )->ignore_parameter( 'P1' )->ignore_parameter( 'P2' )->returning( 40 ).
    test_double->decision_table( p1 = 0 p2 = 0 ).
  ENDMETHOD.
  METHOD order_of_calls_bad.
    " Wrong call order: those 2 next lines should be switched
    test_double->demo_returning( ).
    cl_abap_testdouble=>configure_call( test_double )->returning( 97 ).
    result = cut->zif_atd_demo~demo_returning( ).
    " The result is 0 instead of 97 because of the wrong call order
    assert_equals( exp = 0 act = result ).
  ENDMETHOD.
  METHOD order_of_calls_bad2.
    DATA lx2 TYPE REF TO cx_atd_exception.
    DATA(callconfig) = cl_abap_testdouble=>configure_call( test_double ).
    test_double->demo_returning( ).
    TRY.
        callconfig->returning( 97 ).
        result = cut->zif_atd_demo~demo_returning( ).
      CATCH cx_atd_exception INTO lx2.
    ENDTRY.
    assert_bound( lx2 ).
    assert_equals( exp = cx_atd_exception=>invalid_call
                   act = lx2->if_t100_message~t100key ).
  ENDMETHOD.
  METHOD raise_event_good.
    SET HANDLER on_action_done FOR test_double.
    cl_abap_testdouble=>configure_call( test_double
          )->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( 50 ) ) ) ).
    " the 2 events should be triggered when the method RAISE_EVENT is called
    test_double->demo_raise_event( ).
    cut->zif_atd_demo~demo_raise_event( ).
    assert_equals( exp = 70 act = event_param_value ).
  ENDMETHOD.
  METHOD on_action_done.
    " This method is used for testing the method RAISE_EVENT.
    event_param_value = event_param_value + param_name.
  ENDMETHOD.
  METHOD max_test_doubles.
    DATA: test_double TYPE REF TO object,
          lx          TYPE REF TO cx_sy_generate_subpool_full.
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_ABAP_LOG_ST_TYPES' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_AMDP_DATAPREVIEW' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_ATOM_LINK_MAPPER' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_ATOM_UTILITY' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_BADI_IMP_REG_SHM_DATA' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_CHECK_LIST' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_CHECK_REPORTER' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_CHECK_RUN_RESPONSE' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_CODE_COMPLETION_TYPE' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_COMPATIBILITY_PROVIDER' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_COMPATIBILITY_REGISTRY' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_CTS_MANAGEMENT' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_CT_COMPATIBILITY_GRAPH' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DATAPREVIEW_RES_CO' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DATA_PROVIDER' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DATA_PROVIDER_METADATA' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DATA_PROVIDER_REGISTRY' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DISCOVERY_COLLECTION' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DISCOVERY_PROVIDER' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DISCOVERY_REGISTRY' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DISCOVERY_WORKSPACE' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DISC_REST_RC_REGISTRY' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DP_CDS_RES_CO' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DP_CODE_COMPL_RES_CO' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DP_DBG_AMDP' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DP_DBG_AMDP_RES_CO' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_DP_FREESTYLE_RES_CO' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_EXCEPTION_PROPERTIES' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_EXC_SUBTYPE_CONSTANTS' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_FEED_METADATA' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_FEED_PROVIDER' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_FEED_REGISTRY' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_FQL_ARGUMENT_LIST' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_FQL_ATTRIBUTE' ).
    test_double ?= cl_abap_testdouble=>create( 'IF_ADT_FQL_DATA_TYPE' ).
    TRY.
        " 37th GENERATE SUBROUTINE POOL
        test_double ?= cl_abap_testdouble=>create( 'IF_ADT_FQL_FACTORY' ).
      CATCH cx_sy_generate_subpool_full INTO lx.
    ENDTRY.
    assert_bound( lx ).
    assert_equals( exp = 'GENERATE_SUBPOOL_DIR_FULL' act = lx->kernel_errid ).
  ENDMETHOD.
ENDCLASS.

1 comment: