Unit tests in ABAP. Part two. Rake
This article focuses on ABAP developers in SAP ERP systems. It contains many platform-specific issues that are uninteresting 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. The global goal is the fullest possible test coverage, as part of the appropriateness of what is happening. Under close supervision - exits.

Under the cut I will give some examples of a rake that you can step on.
Suppose our FM does not do a value substitution, but a check:
There are two problems.
Firstly , if you try to make a direct call:
Then it turns out that the test run crashes with a not very intelligible message:
One could reason that since it had fallen, therefore there was a mistake, and then everything is fine. But this is not so, because the test should be green, not red.
If this were a real exception, then we could enclose the call in the TRY-CATCH construct and check if the exception was actually caught:
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 it in another way.
A third-party observer who does not understand the internal ABAP kitchen could say 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 bumbles inside it.
This is not so, and there are reasons for this:
And it turns out that the CALL FUNCTION construct has an addition:
This supplement is designed specifically for such cases.
And now we write the test this way:
Now the test passes correctly.
Secondly , due to the fact that the error is fuzzy, in this case we cannot prove that the error we needed happened. Inside the FM, one hundred twenty-five different errors can be hidden for different occasions. A good error should have all the necessary attributes: type, class, number, parameters.
So you need to refactor the FM itself a little, and such refactoring will benefit him.
It was:
It became:
BTW: This is called a “High Definition Error”.
And after that we can supplement our test with a check:
BTW: in the standard library there are many different variations of the ASSERT method clarifying the meaning of the method; no methods are visible to sweeten such a pack. However, you can stir up your ASSERT, with sugar and github.
For example, I have an exit EXIT_SAPMF02K_001.
That's just bad luck. All CMOD exits are organized as follows: there is a standard group of functions XF05, in which there is a function module EXIT_SAPMF02K_001, in which there is only the line INCLUDE ZXF05U01, all the necessary code is already written in this inclusion.
So the question is: what to create a unit test for?
It cannot be created on 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 functional modules, since 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 straightforward, and therefore preferred.
All other options are less direct, therefore less preferred. Unit tests are not the place to be tricky for no reason.
Inside the exit, queries to the database can be made, for example:
Such things have always been controversial for unit testing. However, somehow it is necessary to live.
Making queries to the database is a legitimate desire of the developer, especially since the standard does not provide enough information in its interface.
One way: switch the corresponding local variables / structures to the optional exit parameters or global variables in the function group. Accordingly, at the time of the test, you will need to fill them. Unfortunately, here you will need to make changes to the productive code. For instance:
The option does not look very beautiful, an optional parameter (IMPORTING, CHAHGING or even TABLES) would look a little better.
But there are a couple of contraindications:
You can consider another option with pre-filling the database with the necessary data for the test. In some scenarios, this makes sense: for example, if you need to check the lender for residency to post a document, you can simply slip a real lender and not simulate it.
It rarely happens that inside an exit there are no additional attributes of the transmitted object. And to get them, we use an ASSIGN hack of the following form:
And what can unit testing do with such a rude attitude to scope? Nothing. Avoid this if possible.
This is a serious reason for reflection.
You can try to taxi 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 ... Or you can leave this section of the code uncovered ... While 100% coverage is not an end in itself, but you need to test first what can break.
Speaking of “breaking down." Recently, there was a case that after an update in the standard, the original variable changed its type, so the code in the exit broke with the following consequences.
Sometimes in the exit code you can find a check for the transaction code:
As part of our simulation, the transaction code is obtained from the environment, and not from the interface of the exit itself. Therefore, in a unit test, SY-TCODE will show the development transaction SE37.
Options:
And finally: if it was not possible to refactor, then you can completely:
It will work, I don’t see a big crime here.
Enough for today. I take off my safety helmet and take my leave. Thanks for attention.
Continuation can be read here: Unit tests in ABAP. Part three. All the fuss
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. The global goal is the fullest possible test coverage, as part of the appropriateness of what is happening. Under close supervision - exits.

Under the cut I will give some examples of a rake that you can step on.
Rake the first. Error processing.
Suppose our FM does not do a value substitution, but a check:
function zfi_bte_00001120.
if ls_bseg-zuonr eq space.
message ‘Поле Присвоение обязательно для заполнения’ type ‘E’.
endif.
endfunction.
There are two problems.
Firstly , 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 crashes with a not very intelligible message:
Exception error
One could reason that since it had fallen, therefore there was a mistake, and then everything is fine. But this is not so, because the test should be green, not red.
If this were a real exception, then we could enclose the call in the TRY-CATCH construct and check if the exception was actually 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 it in another way.
A third-party observer who does not understand the internal ABAP kitchen could say 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 bumbles inside it.
This is not so, and there are reasons for this:
- We cannot change the interface of this FM in any way, because we do not call it. And we cannot fix the place of his call, because it means “breaking the standard”. This is a feature of exits.
- You should not enter into the FM technical optional parameters in the style of THIS_IS_TEST and TEST_RESULT, and then do various actions inside the FM based on these parameters. Such a crutch will do its job, but it is very harmful to clog the productive code with actions that are needed only for the test.
And it turns out that the CALL FUNCTION construct has an addition:
... EXCEPTIONS ... error_message = n_error ...
This supplement is designed specifically for such cases.
And now we write the test this way:
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 fuzzy, in this case we cannot prove that the error we needed happened. Inside the FM, one hundred twenty-five different errors can be hidden for different occasions. A good error should have all the necessary attributes: type, class, number, parameters.
So you need to refactor the FM itself a little, and such 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 supplement our test with a check:
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 clarifying the meaning of the method; no methods are visible to sweeten such a pack. However, you can stir up your ASSERT, with sugar and github.
Rake Two: CMOD
For example, I have an exit EXIT_SAPMF02K_001.
That's just bad luck. All CMOD exits are organized as follows: there is a standard group of functions XF05, in which there is a function module EXIT_SAPMF02K_001, in which there is only the line INCLUDE ZXF05U01, all the necessary code is already written in this inclusion.
So the question is: what to create a unit test for?
It cannot be created on 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 functional modules, since 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 straightforward, and therefore preferred.
All other options are less direct, therefore less preferred. Unit tests are not the place to be tricky for no reason.
Rake the third: Access to a DB
Inside the exit, queries to the database can be made, 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 controversial for unit testing. However, somehow it is necessary to live.
Making queries to the database is a legitimate desire of the developer, especially since the standard does not provide enough information in its interface.
One way: switch the corresponding local variables / structures to the optional exit parameters or global variables in the function group. Accordingly, at the time of the test, you will need to fill them. Unfortunately, here you will need to make changes to the productive code. For instance:
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 beautiful, 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:
- the interface will be different from the standard
- a large number of database queries will inflate the FM interface
You can consider another option with pre-filling the database with the necessary data for the test. In some scenarios, this makes sense: for example, if you need to check the lender for residency to post a document, you can simply slip a real lender and not simulate it.
Rake Four: ASSIGN up
It rarely happens that inside an exit there are no additional attributes of the transmitted object. And to get them, we use an ASSIGN hack of the following form:
assign ('(SAPMF05A)UF05A-STGRD') to .
if sy-subrc = 0.
if = '02'.
…
endif.
endif.
And what can unit testing do with such a rude attitude to scope? Nothing. Avoid this if possible.
This is a serious reason for reflection.
You can try to taxi 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 ... Or you can leave this section of the code uncovered ... While 100% coverage is not an end in itself, but you need to test first what can break.
Speaking of “breaking down." Recently, there was a case that after an update in the standard, the original variable changed its type, so the code in the exit broke with the following consequences.
Rake Fifth: Checking for Transaction Code
Sometimes in the exit code you can find a check for the transaction code:
if ( sy-tcode = 'ASKBN' or sy-tcode = 'ASKB' ) and ls_bkpf-blart = 'AC'.
…
endif.
As part of our simulation, the transaction code is obtained from the environment, and not from the interface of the exit itself. Therefore, in a unit test, SY-TCODE will show the development transaction SE37.
Options:
- First of all: if you think about it, in some cases the transaction verification can be omitted. Often it is redundant.
- Secondly: if you think about it, in some cases you can define a transaction based on other attributes. For example, in the case of the same BTE 1120 exit, there is a sign of BKPF-AWTYP.
And finally: if it was not possible to refactor, then you can completely:
sy-tcode = 'FB01'.
It will work, I don’t see a big crime here.
Enough for today. I take off my safety helmet and take my leave. Thanks for attention.
Continuation can be read here: Unit tests in ABAP. Part three. All the fuss