01-09-2020 8:09 AM
As documented in my recent blog post, I've ventured into ABAP unit tests for the first time and thanks to the help of community members like jacques.nomssi, mike.pokraka and several others, I've already learned a lot and things are working well.
I do have one open question for now and instead of burying it in the comments of my blog post, I decided to start a new Q&A-thread for it. At the moment, I use a Z-table to see if specific users are allowed to perform some actions and this is working as it should and I also have suitable test cases via mocking the table access. As I'm still in prototyping mode in a sandbox system, this was the easiest way to implement it and already better than the hard-coded check in the code I want to replace.
When soon moving the code to the actual development system, I want to switch this logic to a proper authorization object and check against that if a user is allowed to perform an action. So, my question is: how can I then "mock" the authority check during unit testing?
I already searched for an answer, but even though I found some other interesting articles along the way (i. e. ABAP Unit Best Practices in the Wiki or Jacques' blog post Working with ABAP Unit Tests) none of them contained the answer to my specific question. So, is there one which will work in a NW 7.50 system and not be too involved to implement? I could for example imagine doing something along the lines of another interface definition (one for auth instead of table selects) but am not sure if that is the best option.
Thanks much and Cheers
Baerbel
01-09-2020 9:20 AM
OK, so let start with simple example.
First I need an interface
INTERFACE if_authorization_checker.
METHODS check_tcode
IMPORTING
iv_transaction TYPE sytcode
RETURNING
VALUE(rv_result) TYPE abap_bool.
ENDINTERFACE.
I have my real class that test the authorisation
CLASS lc_authorization_checker DEFINITION.
PUBLIC SECTION.
INTERFACES if_authorization_checker.
ENDCLASS.
CLASS lc_authorization_checker IMPLEMENTATION.
METHOD if_authorization_checker~check_tcode.
AUTHORITY-CHECK OBJECT 'S_TCODE' ID 'TCD' FIELD iv_transaction.
rv_result = SWITCH #( sy-subrc WHEN 0 THEN abap_true
ELSE abap_false ).
ENDMETHOD.
ENDCLASS.
But I have also a Mock for the same interface
CLASS lc_authorization_checker_mock DEFINITION.
PUBLIC SECTION.
INTERFACES if_authorization_checker.
METHODS set_tcode_result
IMPORTING
iv_result TYPE abap_bool.
DATA result_tcode TYPE abap_bool.
ENDCLASS.
CLASS lc_authorization_checker_mock IMPLEMENTATION.
METHOD if_authorization_checker~check_tcode.
rv_result = result_tcode.
ENDMETHOD.
METHOD set_tcode_result.
result_tcode = iv_result.
ENDMETHOD.
ENDCLASS.
the mock method contains also a global variable RESULT_TCODE. This variable just contains the result I should received.
The mock method needs also a method to populate this variable.
You need now to inject this class into your final class.
CLASS lc_my_beautiful_class DEFINITION.
PUBLIC SECTION.
METHODS constructor
IMPORTING
io_authorization_checker TYPE REF TO if_authorization_checker OPTIONAL.
METHODS my_beautiful_method
RETURNING
VALUE(rv_result) TYPE abap_bool.
DATA o_authorization_checker TYPE REF TO if_authorization_checker.
ENDCLASS.
CLASS lc_my_beautiful_class IMPLEMENTATION.
METHOD constructor.
o_authorization_checker = COND #( WHEN io_authorization_checker IS BOUND THEN io_authorization_checker
ELSE NEW lc_authorization_checker( ) ).
ENDMETHOD.
METHOD my_beautiful_method.
rv_result = o_authorization_checker->check_tcode( sy-tcode ).
ENDMETHOD.
ENDCLASS.
So, I have an optional attribute in the constructor in my final class. This constructor determine if the paramter is not bound to create the real one.
(in pure Clean Code, you will need a Factory, but it will make the code little bit complex)
Now in your test class, you need to create an instance of the MOCK, and inject using the constructor.
So it is little bit complex here
CLASS ltc_my_beautiful_class DEFINITION
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT
FINAL.
PUBLIC SECTION.
METHODS test_my_beautiful_method FOR TESTING.
ENDCLASS.
CLASS ltc_my_beautiful_class IMPLEMENTATION.
METHOD test_my_beautiful_method.
" Given I have an instance of my tested class.
DATA o_authorization TYPE REF TO if_authorization_checker.
DATA o_authorization_mock TYPE REF TO lc_authorization_checker_mock.
o_authorization_mock = NEW #( ).
o_authorization ?= o_authorization_mock.
DATA(o_cut) = NEW lc_my_beautiful_class( o_authorization ).
" Given the result should be positive
o_authorization_mock->set_tcode_result( abap_true ).
" When I test my method
DATA(rv_authorization_result) = o_cut->my_beautiful_method( ).
" Then nothing append
cl_abap_unit_assert=>assert_true(
act = rv_authorization_result ).
ENDMETHOD.
ENDCLASS.
O_Authorization contains a type corresponding to the Interface expected by the constructor of my final class.
O_Authorization_Mock contains the real mock class, with the possibility to push the result expected
01-09-2020 8:37 AM
Hi Bärbel,
this is a good question, I have exactly the same problem today 🙂
I was testing authorization with real value, and it is a bad practice. Just because my test could failed if my authorization change someday. Or if somebody with more authoriation test the code.
In pure Clean Code point of view, I think you have to create a dedicated class for authorisation control, with interface. And create a simple mock of this class. But that means you cannot create Unit Test for this specific class.
Fred
01-09-2020 8:58 AM
Why not wrapping it into a method that you mock?
result = zcl_auth_checker=>get( )->check( ... ).
01-09-2020 9:03 AM
sandra.rossi
Hi Sandra - I'm sorry, but your response is too cryptic for me to understand as I'm thus far just scratching the surface of both ABAP OO and unit testing.
01-09-2020 9:20 AM
OK, so let start with simple example.
First I need an interface
INTERFACE if_authorization_checker.
METHODS check_tcode
IMPORTING
iv_transaction TYPE sytcode
RETURNING
VALUE(rv_result) TYPE abap_bool.
ENDINTERFACE.
I have my real class that test the authorisation
CLASS lc_authorization_checker DEFINITION.
PUBLIC SECTION.
INTERFACES if_authorization_checker.
ENDCLASS.
CLASS lc_authorization_checker IMPLEMENTATION.
METHOD if_authorization_checker~check_tcode.
AUTHORITY-CHECK OBJECT 'S_TCODE' ID 'TCD' FIELD iv_transaction.
rv_result = SWITCH #( sy-subrc WHEN 0 THEN abap_true
ELSE abap_false ).
ENDMETHOD.
ENDCLASS.
But I have also a Mock for the same interface
CLASS lc_authorization_checker_mock DEFINITION.
PUBLIC SECTION.
INTERFACES if_authorization_checker.
METHODS set_tcode_result
IMPORTING
iv_result TYPE abap_bool.
DATA result_tcode TYPE abap_bool.
ENDCLASS.
CLASS lc_authorization_checker_mock IMPLEMENTATION.
METHOD if_authorization_checker~check_tcode.
rv_result = result_tcode.
ENDMETHOD.
METHOD set_tcode_result.
result_tcode = iv_result.
ENDMETHOD.
ENDCLASS.
the mock method contains also a global variable RESULT_TCODE. This variable just contains the result I should received.
The mock method needs also a method to populate this variable.
You need now to inject this class into your final class.
CLASS lc_my_beautiful_class DEFINITION.
PUBLIC SECTION.
METHODS constructor
IMPORTING
io_authorization_checker TYPE REF TO if_authorization_checker OPTIONAL.
METHODS my_beautiful_method
RETURNING
VALUE(rv_result) TYPE abap_bool.
DATA o_authorization_checker TYPE REF TO if_authorization_checker.
ENDCLASS.
CLASS lc_my_beautiful_class IMPLEMENTATION.
METHOD constructor.
o_authorization_checker = COND #( WHEN io_authorization_checker IS BOUND THEN io_authorization_checker
ELSE NEW lc_authorization_checker( ) ).
ENDMETHOD.
METHOD my_beautiful_method.
rv_result = o_authorization_checker->check_tcode( sy-tcode ).
ENDMETHOD.
ENDCLASS.
So, I have an optional attribute in the constructor in my final class. This constructor determine if the paramter is not bound to create the real one.
(in pure Clean Code, you will need a Factory, but it will make the code little bit complex)
Now in your test class, you need to create an instance of the MOCK, and inject using the constructor.
So it is little bit complex here
CLASS ltc_my_beautiful_class DEFINITION
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT
FINAL.
PUBLIC SECTION.
METHODS test_my_beautiful_method FOR TESTING.
ENDCLASS.
CLASS ltc_my_beautiful_class IMPLEMENTATION.
METHOD test_my_beautiful_method.
" Given I have an instance of my tested class.
DATA o_authorization TYPE REF TO if_authorization_checker.
DATA o_authorization_mock TYPE REF TO lc_authorization_checker_mock.
o_authorization_mock = NEW #( ).
o_authorization ?= o_authorization_mock.
DATA(o_cut) = NEW lc_my_beautiful_class( o_authorization ).
" Given the result should be positive
o_authorization_mock->set_tcode_result( abap_true ).
" When I test my method
DATA(rv_authorization_result) = o_cut->my_beautiful_method( ).
" Then nothing append
cl_abap_unit_assert=>assert_true(
act = rv_authorization_result ).
ENDMETHOD.
ENDCLASS.
O_Authorization contains a type corresponding to the Interface expected by the constructor of my final class.
O_Authorization_Mock contains the real mock class, with the possibility to push the result expected
01-09-2020 10:00 AM
Thanks, Fred!
In going with a baby-steps approach, I think I'll do something along the lines you suggest but "cut some corners" for now while prototyping this.
I already have an interface and class definition for the table selects I need:
interface ZIF_CHECK_WB_ACTION_DAO
PUBLIC.
METHODS:
get_srcsystem_from_tadir
IMPORTING
i_transport_object TYPE e071
RETURNING
VALUE(r_srcsystem) TYPE tadir-srcsystem,
entry_exists_zbc_ddic_check
IMPORTING
i_check_title TYPE z_field
i_checked_entity TYPE data
RETURNING
VALUE(active_entry_found) TYPE abap_bool.
endinterface.
CLASS zcl_check_wb_action_dao DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
INTERFACES zif_check_wb_action_dao.
PROTECTED SECTION.
PRIVATE SECTION.
ENDCLASS.
CLASS zcl_check_wb_action_dao IMPLEMENTATION.
METHOD zif_check_wb_action_dao~entry_exists_zbc_ddic_check.
"----------------------------------------------------------------------
" Method ZIF_CHECK_WB_ACTION_DAO~ENTRY_EXISTS_ZBC_DDIC_CHECK (public)
"----------------------------------------------------------------------
" IMPORTING i_check_title TYPE z_field
" i_checked_entity TYPE data
" RETURNING VALUE(active_entry_found) TYPE abap_bool
"----------------------------------------------------------------------
DATA: lv_char40 TYPE char40.
CLEAR active_entry_found.
lv_char40 = i_checked_entity.
SELECT SINGLE @abap_true
FROM zbc_ddic_check
INTO @active_entry_found
WHERE check_title EQ @i_check_title
AND checked_entity EQ @lv_char40
AND check_active EQ @abap_true.
ENDMETHOD.
METHOD zif_check_wb_action_dao~get_srcsystem_from_tadir.
"----------------------------------------------------------------------
" Method ZIF_CHECK_WB_ACTION_DAO~GET_SRCSYSTEM_FROM_TADIR (public)
"----------------------------------------------------------------------
" IMPORTING i_transport_object TYPE e071
" RETURNING VALUE(r_srcsystem) TYPE tadir-srcsystem
"----------------------------------------------------------------------
CLEAR r_srcsystem.
SELECT SINGLE srcsystem
INTO @r_srcsystem
FROM tadir
WHERE object = @i_transport_object-object
AND obj_name = @i_transport_object-obj_name(40).
ENDMETHOD.
ENDCLASS.
The local test class makes use of this:
*"* use this source file for your ABAP unit test classes
CLASS ltd_dao DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_check_wb_action_dao.
DATA:
tadir TYPE STANDARD TABLE OF tadir WITH DEFAULT KEY,
zbc_ddic_check TYPE STANDARD TABLE OF zbc_ddic_check WITH DEFAULT KEY.
ENDCLASS.
CLASS ltd_dao IMPLEMENTATION.
METHOD zif_check_wb_action_dao~get_srcsystem_from_tadir.
READ TABLE tadir WITH KEY object = i_transport_object-object
obj_name = i_transport_object-obj_name(40)
INTO DATA(tadir_entry).
r_srcsystem = tadir_entry-srcsystem.
ENDMETHOD.
METHOD zif_check_wb_action_dao~entry_exists_zbc_ddic_check.
DATA: lv_char40 TYPE char40.
CLEAR active_entry_found.
lv_char40 = i_checked_entity.
READ TABLE zbc_ddic_check WITH KEY check_title = i_check_title
checked_entity = lv_char40
check_active = abap_true
INTO DATA(zbc_ddic_check_entry).
active_entry_found = zbc_ddic_check_entry-check_active.
ENDMETHOD.
ENDCLASS
So, instead of creating (yet) another global class and interface right away, I'll simply add another method to the ones I already have - well realising that the auth-check shouldn't really be in a class meant for table access. Once things work I'll move the logic to a class/interface of its own.
Cheers
Bärbel
01-09-2020 10:15 AM
But you test in your AbapUnit another class ?
The solution, is less smart just because a Mock should be used everytime you have to test a class using your data selection. with your solution, you will have to rewrite this code in the futur class.
So it should be global.
01-09-2020 10:32 AM
frdric.girod
Hi Fred,
the class I'm actually checking with the unit tests is ZCL_CHECK_WB_ACTION. It does the table selects via the interface, so - for now - it won't matter much (I think) if the auth check is also handled via the same interface in an additional method. I'll have to change parts of the code anyway once I move it from the sandbox to the real dev system as I for example at the moment don't have proper error messages defined and it's possible that some object names will also change. I have quite a few "TODO" pseudo comments in my code as of right now to not forget what all I need to tweak.
I'm running against a bit of a deadline so to speak as the sandbox system I have the development in is going to be "reset" middle of next week, so I'm trying to not let the perfect be the enemy of the good, while still getting valuable information from the prototyping.
Cheers
Bärbel
01-09-2020 9:29 AM
In my example above, CHECK would execute AUTHORITY-CHECK in the productive code, or would execute the test double in test.
You can mock anything:
Wrap the code to mock in a method, eventually put the method in a dedicated class as in your case (ZCL_AUTH_CHECKER)
Create a test double (class) which will "redefine" the method CHECK to return the result that you first inject. In your case, you may inject an internal table of fake authorizations (object, field, value), and the redefined CHECK would read the internal table.
In your test class you inject the test double into ZCL_AUTH_CHECKER, you inject the fake authorizations and you call your code under test.
01-09-2020 9:31 AM
You describe a great scenario of what unit testing is all about. In more generic terms: component A (your application) depends on component B (authority check). If the Depended On Component (DOC) is not yet available, you can mock or stub it, or develop an interim solution (Z-Table). A good approach to parallel or incremental development!
If your unit test abstracts the DOC, you should ideally be able to switch it with no changes to any test or high-level product code. This is what makes good unit tests a safety net.
The fact that you are facing a problem could indicate that your test is possibly at too low a level, which is a common tendency, and is also one of the things that make unit testing more cumbersome than it should be. It's a good reason why testing private methods should usually be avoided.
So in your scenario, I would have added the auth check in one method or class (as Sandra's answer suggests), and your unit tests mock this. The product code initially looks up a Z-Table but later performs an auth check. As long as the interface to the method/class remains the same, and your unit test mocks this interface, no changes to the test should be required when you change the underlying functionality. This is where we see the real benefits of unit testing!
01-09-2020 9:47 AM
Thanks, Mike!
You'll be happy to read that even though I started out with unit tests also for private methods I was eventually able to get rid of all of them and now only have unit tests for public methods with a test coverage of 100%.
Cheers
Bärbel
01-10-2020 2:00 PM
Update:
Thanks to the feedback from sandra.rossi, mike.pokraka and frdric.girod I went ahead and implemented an additional class and interface for the auth checks but did it along the lines of what I already had to make the table selects testable (code snippets only from the local test class):
[...]
CLASS ltd_auth DEFINITION FOR TESTING.
"Local test class to mock authority check routine
PUBLIC SECTION.
INTERFACES: zif_check_wb_action_auth.
DATA:
zbc_ddic_check TYPE STANDARD TABLE OF zbc_ddic_check WITH DEFAULT KEY.
ENDCLASS.
CLASS ltd_auth IMPLEMENTATION.
METHOD zif_check_wb_action_auth~user_has_ddic_auth.
"For the mocking of the auth-check we use special entries in internal represenation of ZBC_DDIC_CHECK_ENTRY
"This simulates a user for whom the auth-check fails but who can do DDIC-changes via an entry in the Z-table anyway
"(but most likely temporarily).
DATA: lv_char40 TYPE char40.
CLEAR r_result.
lv_char40 = i_uname.
READ TABLE zbc_ddic_check WITH KEY check_title = 'DDIC-USER-NO-AUTH'
checked_entity = lv_char40
check_active = abap_true
INTO DATA(zbc_ddic_check_entry).
IF zbc_ddic_check_entry-check_active EQ abap_true.
r_result = abap_true.
ELSE.
r_result = abap_false.
ENDIF.
ENDMETHOD.
ENDCLASS.
[...]
CLASS ltc_action DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS.
"Local test class to test the checking logic
PRIVATE SECTION.
DATA:
cut TYPE REF TO zcl_check_wb_action,
dao TYPE REF TO ltd_dao,
auth TYPE REF TO ltd_auth. "<--- Added
[...]
ENDCLASS.
CLASS ltc_action IMPLEMENTATION.
METHOD setup.
dao = NEW #( ).
auth = NEW #( ). "<--- Added
cut = NEW #( ).
cut->_dao = dao. " Injection table selects
cut->_auth = auth." Injection auth-check "<---Added
[...]
"Fill mock internal table for ZBC_DDIC_CHECK with sample data for testing auth check
auth->zbc_ddic_check = VALUE #( ( check_title = 'DDIC-USER-NO-AUTH'
checked_entity = 'DDICNOAUTH' check_active = abap_true )
).
The test methods themselves remaind unchanged. I did get some errors while adding the auth-check logic and re-running the tests regularly. Initially I missed that I had to (obvious in hindsight!) define and implement the auth-check interface in the test class, leading to technical issues when running the tests. Once that was straightened out, I did get an actual failure for one of the tests until I realised that I had to also provide test data for the internal mock of the Z-table for the auth-check logic (auth->zbc_ddic_check).
Thanks all for your help!