📜 ⬆️ ⬇️

Unit Tests in ABAP. Part two. Rake

This article is focused on ABAP developers in SAP ERP systems. It contains many platform-specific points that are of little interest or even controversial for developers using other platforms.

This is the second part of the publication. You can read the beginning here: Unit tests in ABAP. Part one. First test

The first step has been taken. Now we need to expand and deepen our offensive. Global goal - the most complete test coverage, as part of the appropriateness of what is happening. Under close supervision - exit.


')
Under the cut, I will give a few examples of rakes that can be stepped on.


Grablya first. Error processing.


Suppose our FM does not replace values, but checks:
function zfi_bte_00001120. if ls_bseg-zuonr eq space. message '    ' type 'E'. endif. endfunction. 


There are two problems.
First , if you try to make a direct call:
 call function 'ZFI_BTE_00001120' tables t_bkpf = t_bkpf t_bseg = t_bseg t_bkpfsub = t_bkpfsub t_bsegsub = t_bsegsub. 


Then it turns out that the test run falls with a not very clear message:
Exception Error <CX_AUNIT_UNCAUGHT_MESSAGE>

It was possible to judge that once fell, therefore there was a mistake, and that means everything is fine. But this is not so, because the test must be greenish, not red.

If this were a real exception, then one could conclude a call in the TRY-CATCH construct and check whether the exception is really caught:

 try. call function 'ZFI_BTE_00001120'. catch CX_AUNIT_UNCAUGHT_MESSAGE. lv_catched = 'X'. endtry cl_abap_unit_assert=>assert_true( lv_catched ). 


In this case, the exception occurs in the ABAP Unit engine itself, and not in the testing or testing code. Therefore, it is necessary to catch him in another way.

An outside observer who does not understand the ABAP internal kitchen could state that in this case it is necessary to refactor the functional module itself so that it directly returns an error, and not so that this error bumps inside it.

This is not the case, and there are reasons for this:

And it turns out that the CALL FUNCTION construct has an addition:
... EXCEPTIONS ... error_message = n_error ...

This addition is intended for such cases.

And now we are writing a test like this:
 call function 'ZFI_BTE_00001120' tables t_bkpf = t_bkpf t_bseg = t_bseg t_bkpfsub = t_bkpfsub t_bsegsub = t_bsegsub. exceptions error_message = 99. cl_aunit_assert=>assert_subrc( act = sy-subrc exp = 99 ). 


Now the test passes correctly.

Secondly , due to the fact that the error is not clear, then in this case we cannot prove that exactly the error we needed occurred. Inside the FM can be hidden one hundred and twenty-five different errors for different occasions. A good error should have all the necessary attributes: type, class, number, parameters.

So you need a little more refactor FM itself, and this refactoring will benefit him.

It was:
 message '    ' type 'E'. 

It became:
 message e001(zfi_subst). "     

BTW: This is called a “high definition error”.

And after that we can add our test with the test:
  cl_aunit_assert=>assert_equals( act = sy-msgty exp = 'E' ). cl_aunit_assert=>assert_equals( act = sy-msgid exp = 'ZFI_SUBST' ). cl_aunit_assert=>assert_equals( act = sy-msgno exp = '001' ). 

BTW: in the standard library there are many different variations of the ASSERT method that clarify the meaning, there are no methods to sweeten just such a pack. However, you can stir up your ASSERT, with sugar and github.

A rake the second: CMOD


I have for example the exit EXIT_SAPMF02K_001.

That's just bad luck. All CMOD exits are organized as follows: there is a standard group of XF05 functions, in which there is a function module EXIT_SAPMF02K_001, in which there is only the line INCLUDE ZXF05U01, all the necessary code is written in it already.

That's the question: what to create a unit test?

It cannot be created for a standard group of functions, because for this you will need to modify it, which is not comme il faut.

There are options.

You can make copies of the functional modules, because inside the FM there is only one line of code that will never change. After that, unit tests can be written on these Z-functions. This option is simple and direct, and therefore preferred.

All other options are less straightforward, therefore less preferred. Unit tests are not the place to be cunning without reason.

The third rake: Access to the database


Inside the exit, queries can be made to the database, for example:
 if ls_bkpf-awtyp = 'TRAVL' and ls_bkpf-xblnr ne space. select single * from bkpf into ls_bkpf_st where bukrs = … and xblnr = ls_bkpf-xblnr and awtyp = 'TRAVL'. if sy-subrc = 0. ... endif. endif. 


Such things have always been considered controversial for unit testing. However, it is necessary to live somehow.
Making requests to the database is a legitimate desire of the developer, especially since the standard does not provide enough information in its interface.

One of the ways is to switch the relevant local variables / structures to optional exit parameters or global variables in the function group. Accordingly, at the time of the test will need to fill them. Unfortunately, there will need to make changes to the productive code. For example:
  if gs_bkpf_st is initial. select single * from bkpf into ls_bkpf_st… else. ls_bkpf_st = gs_bkpf_st. endif. if ls_bkpf_st is not initial. … endif. 


The option does not look very nice; an optional parameter (IMPORTING, CHAHGING or even TABLES) would look a little better.
  if p_bkpf_st is supplied. ls_bkpf_st = p_bkpf_st. else. select single * from bkpf into ls_bkpf_st… endif. if ls_bkpf_st is not initial. … endif. 


But there are a couple of contraindications:


You can consider another option with the preliminary filling of the database necessary for the test data. In some scenarios, this makes sense: for example, if to post a document, you need to check the lender for residency, then you can simply slip the real lender, and not engage in its simulation.

Fourth Rake: ASSIGN up


Occasionally it happens that there are no additional attributes of the transmitted object inside the exit. And to get them, we use a hack with an ASSIGN of the following form:
  assign ('(SAPMF05A)UF05A-STGRD') to <stgrd>. if sy-subrc = 0. if <stgrd> = '02'. … endif. endif. 

And what can unit testing do with such a rough attitude to the scope? Nothing. If possible, avoid this.

This is a serious reason to think.
You can try to steer as in the previous rake, you can try to find a more suitable exit, you can try to rely on other parameters, you can try to ensure the transfer of the necessary parameters inside the declared exit interface ... And you can leave this piece of code uncovered ... While 100% coverage is not an end in itself, but you need to test first what may break.

Speaking of "break". Recently there was a case that after an update in the standard, the original variable changed its type, so the code in the exit was broken with the ensuing consequences.

Fifth Rule: Check for Transaction Code


Sometimes in the exit code you can find a check on the transaction code:
 if ( sy-tcode = 'ASKBN' or sy-tcode = 'ASKB' ) and ls_bkpf-blart = 'AC'. … endif. 


Within our simulation, the transaction code is obtained from the environment, and not from the interface of the exit itself. Therefore, in the unit test, the SY-TCODE will show the development transaction SE37.

Options:

And finally: if it didn’t succeed in refrection, then it is possible to completely:
  sy-tcode = 'FB01'. 

It will work, I do not see a big crime here.

For now, that's enough. I take off my protective helmet and take my leave. Thanks for attention.

Continued, you can read here: Unit tests in ABAP. Part Three Every kind of fuss

Source: https://habr.com/ru/post/273569/


All Articles