Application Development Discussions
Join the discussions or start your own on all things application development, including tools and APIs, programming models, and keeping your skills sharp.
cancel
Showing results for 
Search instead for 
Did you mean: 

Test Double Framework raises exception when mocking a Exporting Parameter

thalesvb
Active Contributor

Hello community,

While doing a transition from local mock implementations to Test Double Framework, I faced a problem when using it to double methods with Exporting Parameter on its interface. When the test method import values from a 'fresh' double instance, the first call to a method from this instance when it generates the double class, a CX_ATD_EXCEPTION is raised.

I even found this blog post introducing the framework, but the examples for Exporting Parameters didn't imported its values in test method. Copying the code to system and receiving them triggered the same framework exception.

Strangely, when calling the method twice, but only receiving its value in the second call, it works as expected.

Below is a sample code of this behavior. A simple global interface:

interface zif_test_df public .
  methods is_account_open
    importing i_number         type i
    exporting value(e_is_open) type abap_bool .
endinterface.

The unit test code:

report zp_test_df.

class ltc_test definition risk level harmless duration short for testing.
  private section.
    methods setup.
    methods fails_when_importing_firstcall for testing.
    methods passes_when_importng_secndcall for testing.

    data m_mock type ref to zif_test_df.
    data m_result type abap_bool.
endclass.

class ltc_test implementation.
  method setup.
    m_mock ?= cl_abap_testdouble=>create( 'ZIF_TEST_DF' ).
    cl_abap_testdouble=>configure_call( m_mock )->set_parameter( name = 'E_IS_OPEN' value = abap_true ).
  endmethod.

  method fails_when_importing_firstcall.
    " Raises CX_ATD_EXCEPTION, with '[ ABAP Testdouble Framework ] Importing parameters not allowed in config call. Use SET_PARAMETER'
    m_mock->is_account_open(
      exporting i_number  = 123
      importing e_is_open = m_result
    ).
  endmethod.

  method passes_when_importng_secndcall.
    " First call, it properly initializes double instance.
    m_mock->is_account_open(
      exporting i_number           = 123
*     importing ev_is_open         = m_result
    ).
    " Second call works, and m_result receives values from double FW.
    m_mock->is_account_open(
      exporting i_number   = 123
      importing e_is_open  = m_result
    ).
  endmethod.
endclass.

Unit test result:

Note that If I switch the interface to use RETURNING instead of EXPORTING, and adapt double framework calls and code to use it, both test methods passes. I'm using NW 7.50 SP02 for this test, but verified the same behavior in a NW 7.31 SP10 instance.

I found this exception is raised from CL_ATD_RECORD_STATE class, method VALIDATE_CONFIGURATION, right below the commentary "1. No exporting parameter allowed in configuration call".

Am I missing some configuration call to work properly with Exporting parameter in it's first call? I didn't find a note mentioning this problem (yet).

Regards.

1 ACCEPTED SOLUTION

Sandra_Rossi
Active Contributor

In fact, in your question, you give the answer : "Strangely, when calling the method twice [...] it works as expected".

Yes, the method is always to be "called twice".

The first call after a "configuration call" is the registration of the values of the input parameters (even if no specific values are needed, for instance, one method with no input parameters, or all of its parameters are to be ignored).

And the next calls (from within the Code Under Test) will return the configured values.

It gives you a wrong feeling that the mocked method is to be called twice, because your code is very special as you are "testing the testing".

In the normal usage, you configure the call by defining the mocked values (returning, exporting, raise exception, raise event), then you setup the values of the input parameters for which the configuration call is valid, then you test your Code Under Test, by passing the test double as an argument, which will call some methods of your test double.

Your code should look like this:

method do_payment. " CODE UNDER TEST
  IF NOT account->is_account_open( i_number ).
    RAISE EXCEPTION TYPE lx_account_closed.
  ENDIF.
endmethod.

method right_way. " FOR TESTING
  " Given the opened account number 123
  " When there's a payment
  " Then there shouldn't be the error "account closed" 

  mocked_account ?= cl_abap_testdouble=>create( 'ZIF_TEST_DF' ).
  " set output values
  cl_abap_testdouble=>configure_call( mocked_account )->set_parameter( name = 'E_IS_OPEN' value = abap_true ).
  " set input values
  mocked_account->is_account_open( exporting i_number = 123 ).

  cut = new transaction( mocked_account ).
  TRY.
      cut->do_payment( account = 123 amount = '15.99' ).
    CATCH lcx_account_closed INTO DATA(lx_account_closed).
  ENDTRY.

  cl_abap_unit_assert=>assert_not_bound( lx_account_closed ).
endmethod.

9 REPLIES 9

johannes_konings
Participant
0 Kudos

For me it seems it is not possible to configure an export parameter.
If you jump from line 386 to 392 it "works".

Sandra_Rossi
Active Contributor

In fact, in your question, you give the answer : "Strangely, when calling the method twice [...] it works as expected".

Yes, the method is always to be "called twice".

The first call after a "configuration call" is the registration of the values of the input parameters (even if no specific values are needed, for instance, one method with no input parameters, or all of its parameters are to be ignored).

And the next calls (from within the Code Under Test) will return the configured values.

It gives you a wrong feeling that the mocked method is to be called twice, because your code is very special as you are "testing the testing".

In the normal usage, you configure the call by defining the mocked values (returning, exporting, raise exception, raise event), then you setup the values of the input parameters for which the configuration call is valid, then you test your Code Under Test, by passing the test double as an argument, which will call some methods of your test double.

Your code should look like this:

method do_payment. " CODE UNDER TEST
  IF NOT account->is_account_open( i_number ).
    RAISE EXCEPTION TYPE lx_account_closed.
  ENDIF.
endmethod.

method right_way. " FOR TESTING
  " Given the opened account number 123
  " When there's a payment
  " Then there shouldn't be the error "account closed" 

  mocked_account ?= cl_abap_testdouble=>create( 'ZIF_TEST_DF' ).
  " set output values
  cl_abap_testdouble=>configure_call( mocked_account )->set_parameter( name = 'E_IS_OPEN' value = abap_true ).
  " set input values
  mocked_account->is_account_open( exporting i_number = 123 ).

  cut = new transaction( mocked_account ).
  TRY.
      cut->do_payment( account = 123 amount = '15.99' ).
    CATCH lcx_account_closed INTO DATA(lx_account_closed).
  ENDTRY.

  cl_abap_unit_assert=>assert_not_bound( lx_account_closed ).
endmethod.

Now I get it: the first double instance call after CONFIGURE_CALL step is to register to what method and which values that configuration applies.

There isn't a class documentation in the Double Framework main class (at least on those system I've tested), so I was a little lost in what was missing.

I made some further tests using the RECEIVING instead EXPORTING in the interface, indeed it does't return the expected value in first call, since it's a configuration call, but it doesn't trigger any exception, unlike the EXPORTING case.

Thanks for your help Sandra!

Regards, Thales

As a general hint, one can simplify this construct a little:

TRY.
      cut->do_payment( account =123 amount ='15.99').
  CATCH lcx_account_closed INTO DATA(lx_account_closed).
ENDTRY.

  cl_abap_unit_assert=>assert_not_bound( lx_account_closed ).

To:

TRY.
      cut->do_payment( account =123 amount ='15.99').
  CATCH lcx_account_closed.
    cl_abap_unit_assert=>fail( ).
ENDTRY.

I think it's appropriate to catch the exception here for clarity reasons, as it's the main point of the test.

BUT, one can take it one step further and declare a top level exception and omit the try-catch completely. This will let the unit test framework handle the exceptions.

methods does_it_work for testing raising cx_static_check.   "<-- catch all
...
method does_it_work.
cut->do_whatever( ). "<-- Unit test runner will fail test on any exceptions
...
endmethod.

I guess my coding style comes from the testing of the opposite case, i.e. when an exception is expected:

TRY.
      cut->do_payment( account = 456 amount = '15.99').
  CATCH lcx_account_closed INTO DATA(lx_account_closed).
ENDTRY.
cl_abap_unit_assert=>assert_bound( lx_account_closed ).
cl_abap_unit_assert=>assert_equals( exp = lcx_account_closed=>lcx_account_closed act = lx_account_closed->textid ).

My comment was intended for general info and not really directed at you 🙂

But whilst we're at it, I think assert=>fail( ) is easier to read and generally write negative tests as:

TRY.
      cut->do_payment( account = 456 amount = '15.99').
      cl_abap_unit_assert=>fail( 'No exception raised' ).

  CATCH lcx_account_closed ##no_handler.
    "As expected
ENDTRY.

0 Kudos

I hope I can still give my opinion 🙂 Currently, I guess I'm fond of the "assert" methods, but not of "fail" if I can avoid it. In the example, fail is not so clear to me, I don't know why. Probably that will change in the future.

Of course! I meant it in the sense that it was for discussion, not a code criticism.

I guess it's a matter of style, I've seen various examples of elaborate coding just to use assert_equals. Flags are my bugbear:

if <cond_a> or <cond_b>. 
lv_error = abap_true.
endif.
cl_abap_unit_assert=>assert_equals( act = lv_error
exp = abap_true ).

Just "fail" in the IF, much cleaner.

0 Kudos

yep, I use FAIL in this exact case.