Skip to Content

I attended the recent openSAP course Writing Testable Code for ABAP. Though I already had been using the ABAP Unit test facility for a few years, I learned through this course that what I knew already was only the tip of the ABAP Unit testing iceberg. The instructors, Dr. Juergen Heymann and Thomas Hammer, both did a magnificent job of compiling the material and presenting it.

One of the items explained in the course that particularly intrigued me was the use of the ABAP Test Double Framework (ATDF). I had not yet explored using this, though months ago I had read the very thorough explanation of its capabilities Paul Hardy provides in his book ABAP to the Future (2nd edition). So I decided to explore actually using this feature with my ABAP Unit tests.

The global class cl_abap_testdouble provides much of this framework. Upon browsing the class I was disappointed to find it has no documentation (not in English, anyway). While searching the web to see whether I could find something available there that could help me, I encountered the blog article by Prajul Meyana titled ABAP Test Double Framework – An Introduction. “Yes!”, I thought, “an introduction is exactly what I need right now.” Indeed, the article is very informative, but I also found some of the examples did not provided enough code to actually exercise the test double activity that was being explained. So I began the process of copying that example code into a small test program and executing it to learn more about how ATDF works. Thus began my adventure into exploring this feature, struggling along the way as I tripped over the problems I encountered and persevering to solve them, eventually reaching the point where I was able to execute all the tests successfully.

From my experience with this test double feature I concluded that perhaps other students who attended Writing Testable Code for ABAP might also find this a daunting feature to learn. Accordingly, I am sharing the code I used (see below) to help me get a better grip on ATDF. Presently it is set so that all ABAP Unit tests invoking the test double will fail, but I have provided comments in the code to help you understand what to do to change the code so that it will work. If you know nothing about it and are interested in pursuing ATDF further, then follow these simple steps:

  1. Copy and paste the code below to a local program in your own ABAP environment, assigning package $TMP.
  2. Activate the copied program. This might require you to edit some of the special characters that do not copy correctly from a blog to ABAP code (such as the quote and apostrophe).
  3. Invoke the ABAP Unit test feature (e.g. in SE38: from menu select Program > Execute > Units) to see that all ABAP Unit tests fail.
  4. Now read the comments and make the recommended changes, one by one, until all the ABAP Unit tests pass.

Along the way you will learn more about how to use the ABAP Test Double Framework to your advantage.

Enjoy,

Jim

The following statements formerly contained in the header comment block were removed from the source code (shown below) to make it more concise, but provides more explanation about some of the nuances of using ATDF.  They otherwise would appear ahead of the “Your Turn – ” comment statement:

" In this case the code under test is an instance of standard SAP global class
" cl_td_expense_manager and the corresponding component on which it depends, for
" which a test double is provided in these examples, is a currency converter object
" that implements the standard SAP global interface if_td_currency_converter.
"
" This demo program represents a simple example of how the ABAP Test Double
" Framework can be used, taking advantage of the following characteristics
" of the components used:
"   o The instance of the code under test -- an expense manager of the type
"     cl_td_expense_manager -- has a simple set of only 2 public methods
"     that can be explicitly invoked:
"     - ADD_EXPENSE_ITEM
"     - CALCULATE_TOTAL_EXPENSE
"     In addition, it has only a single component on which it depends --
"     an instance of a currency converter implementing the interface
"     if_td_currency_converter, an interface which also has a simple set
"     of only 2 public methods:
"     - CONVERT
"     - CONVERT_TO_BASE_CURRENCY
"   o Class cl_td_expense_manager has a constructor method which enables
"     dependency injection of its currency converter, allowing it to be
"     provided with a test double for the currency converter during these
"     ABAP Unit tests.
"
" Current constraints of ATDF -
"
" Currently the ABAP Test Double Framework is based upon the use of a global
" interface to provide the definition of the methods to be simulated by the
" test doubles. Specifically, there is no support for defining test doubles
" for any of these other options:
"   o global classes
"   o local interfaces
"   o local classes
"
" General steps for using ATDF -
"
" The use of ATDF relies on a test double being configured to return
" specific values based on a specific set of parameters accompanying a
" call to a method provided by the test double. This is generally a
" 4-step process:
"   o In step 1, the static method "create" of class cl_abap_testdouble
"     is invoked, passing the name of a global interface implemented by a
"     component on which the code under test is dependent, to create an
"     instance of a test double that can accommodate all the methods
"     defined by the global interface.
"   o In step 2, an instance of the code under test is created, making
"     available to it the test double created in step 1.
"   o In step 3, the static method "configure_call" of class cl_abap_testdouble
"     is invoked, passing the test double instance, and, in its simplest
"     format (see further explanation below), specifying the return values to
"     be used with a test double method call. The actual method call for which
"     these value are to be returned is specified in the next step.
"   o In step 4, the method of the test double that is to return the values
"     specified in step 3 is invoked specifying the parameter values that
"     are to trigger the return values specified in step 3.
" Once established accordingly, calls by the code under test to the methods
" of the component on which it depends will be intercepted by the test double
" and provided with the configured return values. In effect, the code under
" test is oblivious to the fact that it is invoking the methods of a test double.
"
" Technical aspects of cl_abap_testdouble -
"
" There are a variety of ways to invoke static method "configure_call" of class
" cl_abap_testdouble, the simplest of which includes specfying a return value to
" be provided to the caller of the test double method. In fact, a call to
" method "configure_call" does not configure anything, but it does return a
" reference to an instance of if_abap_testdouble_config (that is, an object
" implementing the global interface if_abap_testdouble_config). It is this
" object that gets called to apply some aspect of how the test double is to
" respond to method calls. Interface if_abap_testdouble_config provides the
" following methods that can be invoked against an instance implementing it:
"   o returning
"   o set_parameter
"   o raise exception
"   o raise event
"   o times
"   o ignore_parameter
"   o ignore_all_parameters
"   o and_expect
"   o set_answer
"   o set_matcher
" Most of these methods have a signature indicating to return an instance of
" if_abap_testdouble_config. Accordingly, such an instance can invoke any
" of these methods, and an instance will be returned to the caller reflecting
" the effect of the method call. The returned instance can be the same instance
" updated accordingly or can be a new instance. A subsequent call using the
" instance to a different method will return a new/updated instance also
" reflecting the effect of the called method. Indeed, this process can be
" repeated as long as is necessary to fully configure the response the code
" under test should receive upon invoking a method of the test double.
"
" The Builder design pattern -
"
" An object accommodating such calls conforms to the object-oriented design
" pattern known as the Builder, where a sequence of such method calls results
" in an object becoming "built" to the necessary end state. So, for instance,
" to configure a test double response to indicate the following attributes
" (one that you will see used in this demo):
"   - returning = 80
"   - times = 2
" we could use the following sequence of ABAP statements to construct such
" an object:
"
"   data currency_converter_double type ref to if_td_currency_converter.
"   data test_double_configuration type ref to if_abap_testdouble_config.
"
"   test_double_configuration = cl_abap_testdouble=>configure_call( currency_converter_double ).
"   test_double_configuration = test_double_configuration->returning( 80 ).
"   test_double_configuration = test_double_configuration->times( 2 ).
"
" In the example code above, the field test_double_configuration is a helper variable
" holding the object created/updated with each new method invocation, however ABAP
" enables calling such methods to be strung together without the need for the
" helper variable at all, as in:
"
" cl_abap_testdouble=>configure_call( currency_converter_double )->returning( 80 )->times( 2 ).
"
" With this format, the object returned from the call to cl_abap_testdouble=>configure_call
" is used as the object on which the call to method returning( 80 ) is made, and its returning
" object is subsequently used on the call to method times( 2 ).

 

Source code to copy:

report.
"----------------------------------------------------------------------
" ABAP Test Double Framework (ATDF) demo program.
"
" Jim McDonough - May 6, 2018
"
" Purpose -
"
" This program illustrates the use of the ABAP Test Double Framework with
" ABAP Unit tests for providing test doubles (mock objects, stubs, spies,
" fakes, etc.) as the component(s) on which the code under test depends.
"
" Source material used -
"
" Most of the source code was copied and extensively modified from the
" example ABAP source found in the following blog by Prajul Meyana:
" o https://blogs.sap.com/2015/01/05/abap-test-double-framework-an-introduction/
" Indeed, the source code in that blog is itself mostly a copy of the local
" ABAP Unit test associated with global class cl_td_expense_manager, a class
" provided for the purpose of demonstrating ATDF.  To see the ABAP Unit test
" code written for class cl_td_expense_manager, do the following:
"   o Invoke transaction SE24
"   o Specify object type cl_td_expense_manager, then press Display
"   o From the menu select:
"       Goto > Local Definitions/Implementations > Local Test Classes
"
" Your turn -
"
" All of the tests in this ABAP Unit test demo are currently set to fail.
" In each case, the correction to make the ABAP Unit test pass is a commented
" line following the failing line.  Your task is to run all the ABAP Unit
" tests, see that they fail, and they correct them, one by one, to see that
" the tests all pass.  So follow these steps:
"   o Use the ABAP editor SE38 to make a copy of this program.
"   o Activate the copied program.
"   o Run the ABAP Unit tests of this program in one of the following ways:
"     - Via SE38, select from the menu: Program > Execute > Unit Tests.
"     - Via SE38, press the key combination Ctrl+Shift+F10.
"   o Make a single correction by adjusting the ABAP code.  Each failing line
"     or set of lines is marked with "ABAP Unit failure", and each correct
"     line or set of lines is marked with "ABAP Unit success".
"   o Run the ABAP Unit test again to see that a failing test you changed
"     now passes.
"   o Repeat this process until all test pass.
"----------------------------------------------------------------------

class new_currency_code_observer        definition
                                        final
                                        .
  " This class exists solely to act as an observer responding to the
  " event new_currency_code of interface if_td_currency_converter.
  public section.
    methods      : constructor
                 , get_currency_code
                     returning
                       value(currency_code)
                         type string
                 , handle_new_currency_code
                     for event new_currency_code
                            of if_td_currency_converter
                     importing
                       currency_code
                 .
  private section.
    data         : new_currency_code
                                  type string
                 .
endclass.
class new_currency_code_observer       implementation.
  method constructor.
    " At the start, we want our own value for new currency
    " code to be cleared ...
    clear me->new_currency_code.
    " ... and we want to be able to respond to the raised event
    " new_currency_code of if_td_currency_converter:
    set handler handle_new_currency_code for all instances.
  endmethod.
  method handle_new_currency_code.
    " We are now responding to the raised event
    " new_currency_code of if_td_currency_converter and
    " we want to overwrite our own value for new currency code
    " using the value accompanying the raised event:
    me->new_currency_code         = currency_code.
  endmethod.
  method get_currency_code.
    " Some caller is requesting the new currency code this object
    " has, presumably obtained after having responded to the raised
    " event new_currency_code of if_td_currency_converter:
    currency_code                 = me->new_currency_code.
  endmethod.
endclass.

class currency_exchanger               definition
                                       final
                                       .
  " This class exists only because global class cl_td_expense_manager
  " does not invoke method convert_to_base_currency of interface
  " if_td_currency_converter, one of only two methods this interface
  " specifies.  Whereas the local ABAP Unit test accompanying class
  " cl_td_expense_manager seems to establish a test for invoking method
  " convert_to_base_currency of interface if_td_currency_converter, it
  " is incomplete because the code implemented for cl_td_expense_manager
  " never invokes this interface method.  As such, the ABAP Unit
  " test code written for cl_td_expense_manager to test this call
  " (lines 73 through 80) is never executed to the point where it can be
  " determined whether the test double would respond accordingly.
  public section.
    methods      : constructor
                     importing
                       currency_converter
                         type ref to if_td_currency_converter
                 , exchange_currency
                    importing
                      amount
                        type i
                      source_currency
                        type string
                    exporting
                      base_currency
                        type string
                      base_curr_amount
                        type i
                 .
  private section.
    data         : currency_converter
                     type ref
                       to if_td_currency_converter
                 .
endclass.
class currency_exchanger               implementation.
  method constructor.
    me->currency_converter        = currency_converter.
  endmethod.
  method exchange_currency.
    me->currency_converter->convert_to_base_currency(
      exporting
        amount                    = amount
        source_currency           = source_currency
      importing
        base_currency             = base_currency
        base_curr_amount          = base_curr_amount
      ).
  endmethod.
endclass.

class custom_matcher                   definition.
  public section.
    interfaces   : if_abap_testdouble_matcher
                 .
endclass.
class custom_matcher                   implementation.
  method if_abap_testdouble_matcher~matches.
    data         : act_currency_code
                                  type ref to data
                 , conf_currency_code
                                  type ref to data
                 .
    field-symbols: <act_currency> type string
                 , <conf_currency>
                                  type string
                 .
    result                        = abap_false.
    if method_name eq 'CONVERT'. " Must be specified as upper-case value
      act_currency_code           = actual_arguments->get_param_importing( 'source_currency' ).
      conf_currency_code          = configured_arguments->get_param_importing( 'source_currency' ).
      assign act_currency_code->*  to <act_currency>.
      assign conf_currency_code->* to <conf_currency>.
      if <act_currency>  is assigned and
         <conf_currency> is assigned.
        if <act_currency> cp <conf_currency>.
          result                  = abap_true.
        endif.
      endif.
    endif.
  endmethod.
endclass.

class custom_answer                    definition.
  public section.
    interfaces   : if_abap_testdouble_answer
                 .
endclass.
class custom_answer                     implementation.
  method if_abap_testdouble_answer~answer.
    data         : source_currency_code
                                  type ref to data
                 , target_currency_code
                                  type ref to data
                 , amount         type ref to data
                 .
    field-symbols: <source_currency_code>
                                  type string
                 , <target_currency_code>
                                  type string
                 , <amount>       type  i
                 .
    result->set_param_returning( 00 ).
    if method_name eq 'CONVERT'.  " Must be specified as upper-case value
      source_currency_code        = arguments->get_param_importing( 'source_currency' ).
      target_currency_code        = arguments->get_param_importing( 'target_currency' ).
      amount                      = arguments->get_param_importing( 'amount' ).
      assign source_currency_code->* to <source_currency_code>.
      assign target_currency_code->* to <target_currency_code>.
      assign amount->*               to <amount>.
      if <source_currency_code> is assigned and
         <target_currency_code> is assigned and
         <amount>               is assigned.
        if <source_currency_code> eq 'INR' and
           <target_currency_code> eq 'EUR'.
          result->set_param_returning( <amount> / 80 ).
        endif.
      endif.
    endif.
  endmethod.
endclass.

class abap_test_double_examples        definition
                                       final
                                       for testing
                                       duration short
                                       risk level harmless
                                       .
  private section.
    types        : currency       type string
                 .
    constants    : currency_converter_interface
                                  type seoclsname
                                                 value 'if_td_currency_converter'
                 , euro           type abap_test_double_examples=>currency
                                                 value 'EUR'
                 , indian_rupee
                                  type abap_test_double_examples=>currency
                                                 value 'INR'
                 , us_dollar      type abap_test_double_examples=>currency
                                                 value 'USD'
                 .
    data         : currency_converter_double
                                  type ref to if_td_currency_converter
                 , expense_manager
                                  type ref to cl_td_expense_manager
                 .
    methods      : setup
                 , teardown
                 , simple_configuration
                     for testing raising cx_static_check
                 , configuration_variation_01
                     for testing raising cx_static_check
                 , configuration_variation_02
                     for testing raising cx_static_check
                 , configuration_variation_03
                     for testing raising cx_static_check
                 , configuration_exception
                     for testing raising cx_static_check
                 , configuration_event
                     for testing raising cx_static_check
                 , configuration_times
                     for testing raising cx_static_check
                 , custom_answer
                     for testing raising cx_static_check
                 , custom_matcher
                     for testing raising cx_static_check
                 , verify_test_double_interaction
                     for testing raising cx_static_check
                 .
endclass.
class abap_test_double_examples        implementation.
  method setup.
    " Create both code under test (expense manager) as well as the
    " test double masquerading as the ccmponent on which it depends
    " (currency converter):
    " Create test double object:
    me->currency_converter_double ?= cl_abap_testdouble=>create( me->currency_converter_interface ).
    " Create the object to be tested, injecting the test double upon
    " which it shall depend for the purpose of testing:
    create object me->expense_manager
      exporting
        currency_converter        = me->currency_converter_double
        .
  endmethod.
  method teardown.
    " Destroy both code under test (expense manager) as well as the
    " test double masquerading as the ccmponent on which it depends
    " (currency converter):
    clear: me->currency_converter_double
         , me->expense_manager
         .
  endmethod.
  method simple_configuration.
    constants    : fixed_returning_expense
                                  type i         value 80
                 .
    data         : expected_expense
                                  type i
                 , actual_expense
                                  type i
                 .
    " In this test, the test double always will return the value
    " fixed_returning_expense when the code under test invokes its
    " convert method.

    " Configuring the call to method 'convert' of the test double
    " always to return a known value when invoked by the code under test:
    " Step 1: Set the desired returning value for the called method
    "         of the test double:
    " Step 2: Specify the method to be called and the parameters
    "         associated with the call that will return the value
    "         configured in the previous step:
    " Step 3: Invoke a method upon the code under test that will cause
    "         it to return a value affected by the call it makes
    "         to the test double:
    " Step 4: Assert that the actual value returned by the previous step
    "         is the same as the expected value it should return:

    " Step 1: Set the desired returning value for the called method
    "         of the test double:
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->returning( fixed_returning_expense
      ).
    " Step 2: Specify the method to be called and the parameters
    "         associated with the call that will return the value
    "         configured in the previous step:
    me->currency_converter_double->convert(
      exporting
        amount                    = 100
        source_currency           = me->us_dollar
        target_currency           = me->euro
      ).
    " Step 3: Invoke a method upon the code under test that will cause
    "         it to return an actual value affected by the call it makes
    "         to the test double:
    " 3a) Call the expense manager to provide it with an entry having
    "     an amount and source currency code matching the parameters used
    "     in the previous step to configure the call to the convert method
    "     of the test double:
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 1'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    " 3b) Now that the expense manager has an entry to use, call its
    "     method calculate_total_expense, passing the target currency code
    "     matching the parameters used in step 2 to configure the call
    "     to the convert method of the test double; this will cause
    "     the expense manager to invoke the convert method of the test
    "     double using the same parameters as configured in step 2, and
    "     as a consequence will return the amount fixed_returning_expense:
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " Step 4: Assert that the actual value returned by the previous step
    "         is the same as the expected value it should return:
    expected_expense              = fixed_returning_expense.
    cl_abap_unit_assert=>assert_equals(
      exp                         = expected_expense
      act                         = actual_expense + 1 " ABAP Unit failure
*      act                         = actual_expense     " ABAP Unit success
      ).
  endmethod.
  method configuration_variation_01.
    constants    : fixed_base_currency_amount
                                  type i         value 150
                 .
    data         : currency_exchanger
                                  type ref
                                    to currency_exchanger
                 , base_currency  type string
                 , base_curr_amount
                                  type i
                 .
    " This is a special case.  We are not going to use the code under
    " test provided by the setup method, but are going to instantiate
    " our own object, where the instance of the class to be tested is
    " not one of global class cl_td_expense_manager but is instead a
    " an instance of local class currency_exchanger (see comments in
    " local class currency_exchanger definition for more explanation
    " of the associated issues).  To do this:
    "   1) Destroy the expense manager established by the setup method.
    "   2) Create an instance of currency_exchanger, injecting it with
    "      the currency converter already created by the setup method.

    " 1) Destroy the expense manager established by the setup method:
    clear me->expense_manager.
    " 2) Create an instance of currency_exchanger, injecting it with
    "    the currency converter already created by the setup method:
    create object currency_exchanger
      exporting
        currency_converter        = me->currency_converter_double
        .
    " Configuration for exporting parameters
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->set_parameter( name      = 'base_currency'
                        value     = me->euro
      )->set_parameter( name      = 'base_curr_amount'
                        value     = fixed_base_currency_amount
      ).
    me->currency_converter_double->convert_to_base_currency(
      exporting
        amount                    = 100
        source_currency           = me->us_dollar
      ).
    " Actual method call
    currency_exchanger->exchange_currency(
      exporting
        amount                    = 100
        source_currency           = me->us_dollar
      importing
        base_currency             = base_currency
        base_curr_amount          = base_curr_amount
      ).
    " Assertions
    cl_abap_unit_assert=>assert_equals(
      exp                         = fixed_base_currency_amount
      act                         = base_curr_amount + 1 " ABAP Unit failure
*      act                         = base_curr_amount     " ABAP Unit success
      ).
    cl_abap_unit_assert=>assert_equals(
      exp                         = me->euro
      act                         = me->us_dollar " ABAP Unit failure
*      act                         = base_currency " ABAP Unit success
      ).
  endmethod.
  method configuration_variation_02.
    constants    : fixed_returning_expense
                                  type i         value 55
                 .
    data         : expected_expense
                                  type i
                 , actual_expense
                                  type i
                 .
    " In this test, the test double always will return the value
    " fixed_returning_expense when the code under test invokes its
    " convert method using currency US dollar, regardless of the value
    " specified for the amount parameter.

    " Configuration ignoring one parameter. fixed_returning_expense gets returned if
    " source currency = US dollar, target currency = Euro and any value for amount.
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->returning( fixed_returning_expense
      )->ignore_parameter( 'amount'
      ).
    me->currency_converter_double->convert(
      exporting
        amount                    = 0 "dummy value because amount is a non optional parameter
        source_currency           = me->us_dollar
        target_currency           = me->euro
      ).
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 1'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 2'
        currency_code             = me->us_dollar
        amount                    = 200
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 3'
        currency_code             = me->us_dollar
        amount                    = 400
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    " Actual method call
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " Assertion
    cl_abap_unit_assert=>assert_equals(
      exp                         = expected_expense
      act                         = actual_expense + 1 " ABAP Unit failure
*      act                         = actual_expense     " ABAP Unit success
      ).
  endmethod.
  method configuration_variation_03.
    constants    : fixed_returning_expense
                                  type i         value 55
                 .
    data         : expected_expense
                                  type i
                 , actual_expense
                                  type i
                 .
    " In this test, the test double always will return the value
    " fixed_returning_expense when the code under test invokes its
    " convert method regardless of any parameters values specified.

    " Configuration ignoring all parameters. fixed_returning_expense gets
    " returned for any input.
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->returning( fixed_returning_expense
      )->ignore_all_parameters(
      ).
    me->currency_converter_double->convert(
      exporting
        amount                    = 0 "dummy value
        source_currency           = me->us_dollar "dummy value
        target_currency           = me->euro "dummy value
      ).
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 1'
        currency_code             = 'XYZ' " deliberate invalid currency code
        amount                    = 100
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 2'
        currency_code             = 'ABC' " deliberate invalid currency code
        amount                    = 200
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 3'
        currency_code             = 'HIJ' " deliberate invalid currency code
        amount                    = 400
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    " Actual method call
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " Assertion
    cl_abap_unit_assert=>assert_equals(
      exp                         = expected_expense
      act                         = actual_expense + 1 " ABAP Unit failure
*      act                         = actual_expense     " ABAP Unit success
      ).
  endmethod.
  method configuration_exception.
    data         : actual_expense ##NEEDED
                                  type i
                 , exception      type ref to cx_td_currency_exception
                 .
    " In this test, the test double always will raise the
    " exception cx_td_currency_exception.

    " Instantiate the exception object.
    "   Note: This step would not be necessary in production code since
    "         the act of raising the exception would instantiate this
    "         instance.  With the ABAP Test Double Framework in control
    "         of raising the exception, the ABAP Unit test will fail with
    "         an exception CX_ATD_EXCEPTION accompanied by the text
    "         "[ ABAP Testdouble Framework ] Exception object not bound"
    "         unless there is an object instance already bound to this
    "         reference variable.
    create object exception.
    " Configuration for exception. the specified exception gets raised if
    " amount = -1, source_currency = US dollar and target_currency = Euro
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->raise_exception( exception
      ).
    " The instantiated exception object only needs to remain instantiated
    " for the duration of the preceding configuration call:
    clear exception.
    " ABAP Unit failure due to the inactive ABAP statements from here ...
*    me->currency_converter_double->convert(
*      exporting
*        amount                    = -1
*        source_currency           = me->us_dollar
*        target_currency           = me->euro
*      ).
    " ... to here. ABAP Unit failure
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 1'
        currency_code             = me->us_dollar
        amount                    = -1
      ).
    try.
      " Actual method call
      actual_expense              = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
      " We should never get this far; exception should have been raised
      cl_abap_unit_assert=>fail(
        msg                       = 'cx_td_currency_exception not raised'
        ).
    catch cx_td_currency_exception ##NO_HANDLER.
      " No action; this is the intended path
    endtry.
  endmethod.

 method configuration_event.
   data          : actual_expense ##NEEDED
                                  type i
                 , event_params
                                  type abap_parmbind_tab
                 , event_param    type abap_parmbind
                 , event_observer type ref to new_currency_code_observer
                 .
    field-symbols: <value>        type string
                 .
    " In this test, the test double always will raise the
    " event new_currency_code of if_td_currency_converter.

    " Create event observer:
    create object event_observer.
    " Configuration for event. 'new_currency_code' event gets raised if the
    " source_currency = Indian rupee
    event_param-name              = 'currency_code'.
    create data event_param-value type string.
    assign event_param-value->* to <value>.
    <value>                       = me->indian_rupee.
    insert event_param into table event_params.
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->raise_event( name        = 'new_currency_code'
                      parameters  = event_params
      )->ignore_parameter( 'target_currency'
      )->ignore_parameter( 'amount'
      ).
    " ABAP Unit failure due to the inactive ABAP statements from here ...
*    me->currency_converter_double->convert(
*      exporting
*        amount                    = 00 " dummy value
*        source_currency           = me->indian_rupee
*        target_currency           = '' " dummy value
*      ).
    " ... to here. ABAP Unit failure
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 1'
        currency_code             = me->indian_rupee
        amount                    = 100
      ).
    " Actual method call
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " At this point we do not care what value was returned into
    " field actual_expense.  Instead, we care whether the call caused
    " the test double to raise the event new_currency_code of
    " if_td_currency_converter.  Retrieving the value returned by
    " invoking method get_currency_code of the event observer will
    " tell us whether or not the event was raised and the event
    " observer responded to it:
    cl_abap_unit_assert=>assert_equals(
      exp                         = me->indian_rupee
      act                         = event_observer->get_currency_code( )
      ).
  endmethod.
  method configuration_times.
    data         : expected_expense
                                  type i
                 , actual_expense
                                  type i
                 .
    " In this test, the test double will return values depending
    " on the sequence of calls made to it.  In this case it will
    " be configured to return the value 80 for the first 2 calls
    " where the calling parameters match and to return the value
    " 40 for any subsequent calls where the calling parmaters match.

    " Configuration for returning 80 for 2 times
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->returning( 80
      )->times( 2
      ).
    me->currency_converter_double->convert(
      exporting
        amount                    = 100
        source_currency           = me->us_dollar
        target_currency           = me->euro
      ).
    " Configuration for returning 40 the next time
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->returning( 40
      ).
    me->currency_converter_double->convert(
      exporting
        amount                    = 100
        source_currency           = me->us_dollar
        target_currency           = me->euro
      ).
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 1'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    expected_expense              = expected_expense + 80.
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 2'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    expected_expense              = expected_expense + 80.
    " Add expense item
    me->expense_manager->add_expense_item(
      exporting
        description               = 'Line item 3'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    expected_expense              = expected_expense + 40.
    " Actual method call
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " Assertion
    cl_abap_unit_assert=>assert_equals(
      exp                         = expected_expense
      act                         = actual_expense + 1 " ABAP Unit failure
*      act                         = actual_expense     " ABAP Unit success
     ).
  endmethod.
  method verify_test_double_interaction.
    data         : expected_expense
                                  type i
                 , actual_expense
                                  type i
                 .
    " In this test, the test double itself will be invoked to
    " determine whether it was called using matching calling
    " parameters as many times as we expected it should have
    " been called.

    " Add three expenses. Notice the currency_code
    " specified for each of these calls:
    me->expense_manager->add_expense_item(
      exporting
        description               = 'line item 1'
        currency_code             = me->indian_rupee
        amount                    = 100
      ).
    me->expense_manager->add_expense_item(
      exporting
        description               = 'line item 2'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    expected_expense              = expected_expense + 80.
    me->expense_manager->add_expense_item(
      exporting
        description               = 'line item 3'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    expected_expense              = expected_expense + 80.
    " ABAP Unit failure due to the active ABAP statements from here ...
    me->expense_manager->add_expense_item(
      exporting
        description               = 'line item 4'
        currency_code             = me->us_dollar
        amount                    = 100
      ).
    expected_expense              = expected_expense + 80.
    " ... to here. ABAP Unit failure
    " Configuration of expected interactions.  We expect there
    " will be 2 calls made where the test double will return the
    " value 80 when called with amount 100, source currency US dollar
    " and target currency Euro:
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->returning( 80
      )->and_expect(
      )->is_called_times( 2
      ).
    me->currency_converter_double->convert(
      exporting
        amount                    = 100
        source_currency           = me->us_dollar
        target_currency           = me->euro
      ).
    " Actual method call
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " Assertion
    cl_abap_unit_assert=>assert_equals(
      exp                         = expected_expense
      act                         = actual_expense + 1 " ABAP Unit failure
*      act                         = actual_expense     " ABAP Unit success
      ).
    " Verify interactions on testdouble
    cl_abap_testdouble=>verify_expectations( me->currency_converter_double ).
  endmethod.

  method custom_matcher.
    constants    : fixed_returning_expense
                                  type i         value 80
                 .
    data         : expected_expense
                                  type i
                 , actual_expense
                                  type i
                 , matcher        type ref to custom_matcher
                 .
    " Instantiate matcher object
    create object matcher.
    " Configure test double to use matcher object
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->returning( fixed_returning_expense
      )->set_matcher( matcher
      ).
    me->currency_converter_double->convert(
      exporting
        amount                    = 100
        source_currency           = 'usd*' " currency pattern for US dollar
        target_currency           = me->euro
      ).
    " Add expenses with pattern
    expense_manager->add_expense_item(
      exporting
        description               = 'line item 1'
        currency_code             = 'usdollar' " will conform to expected currency pattern
        amount                    = 100
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    expense_manager->add_expense_item(
      exporting
        description               = 'line item 2'
        currency_code             = 'usdlr' " will conform to expected currency pattern
        amount                    = 100
      ).
    expected_expense              = expected_expense + fixed_returning_expense.
    expense_manager->add_expense_item(
      exporting
        description               = 'line item 3'
        currency_code             = 'dollar' " will not conform to expected currency pattern
        amount                    = 100
      ).
    expected_expense              = expected_expense + fixed_returning_expense. " ABAP Unit failure
*    expected_expense              = expected_expense + 00.                      " ABAP Unit success
    " Actual method call
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " Assertion
    cl_abap_unit_assert=>assert_equals(
      exp                         = expected_expense
      act                         = actual_expense
      ).
  endmethod.

  method custom_answer.
    data         : expected_expense
                                  type i
                 , actual_expense
                                  type i
                 , answer            type ref to custom_answer
                 .

    " Instantiate answer object
    create object answer.
    " Configure test double to use answer object
    cl_abap_testdouble=>configure_call( me->currency_converter_double
      )->ignore_parameter( 'amount'
      )->set_answer( answer
      ).
    me->currency_converter_double->convert(
      exporting
        amount                    =  0
        source_currency           = me->indian_rupee
        target_currency           = me->euro
      ).
    " Add the expense line items
    expense_manager->add_expense_item(
      exporting
        description               = 'line item 1'
        currency_code             = me->indian_rupee
        amount                    = 80
      ).
    expense_manager->add_expense_item(
      exporting
        description               = 'line item 2'
        currency_code             = me->indian_rupee
        amount                    = 240
      ).
    expense_manager->add_expense_item(
      exporting
        description               = 'line item 3'
        currency_code             = me->indian_rupee
        amount                    = 800
      ).
    expense_manager->add_expense_item(
      exporting
        description               = 'line item 4'
        currency_code             = me->indian_rupee
        amount                    = 880
      ).
    " Actual method call
    actual_expense                = me->expense_manager->calculate_total_expense( currency_code = me->euro ).
    " Assertion
    expected_expense              = 25. " ( 80 + 240 + 800 + 880 ) / 80 = 25
    cl_abap_unit_assert=>assert_equals(
      exp                         = expected_expense
      act                         = actual_expense + 1 " ABAP Unit failure
*      act                         = actual_expense     " ABAP Unit success
      ).
  endmethod.
endclass.

 

 

 

To report this post you need to login first.

2 Comments

You must be Logged on to comment or reply to a post.

  1. Alejandro Jacard Plubins

    I’ve been very timid about incorporating ABAP unit in my developments, but have to smile when seeing someone comment and explain code so well, wishing every programmer did the same.

    (0) 

Leave a Reply