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: 

How can authority checks be mocked in Unit Tests?

BaerbelWinkler
Active Contributor
0 Kudos

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

1 ACCEPTED SOLUTION

FredericGirod
Active Contributor

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

11 REPLIES 11

FredericGirod
Active Contributor

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

Sandra_Rossi
Active Contributor
0 Kudos

Why not wrapping it into a method that you mock?

result = zcl_auth_checker=>get( )->check( ... ).

BaerbelWinkler
Active Contributor
0 Kudos

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.

FredericGirod
Active Contributor

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

0 Kudos

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

0 Kudos

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.

0 Kudos

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

Sandra_Rossi
Active Contributor

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.

pokrakam
Active Contributor

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!

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

BaerbelWinkler
Active Contributor

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!