Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
former_member183804
Active Contributor

Hello Community,

ABAP Test Seams are available with the latest SAP_BASIS stack, but what they are good for?

Consider you need to change some legacy code of ancient times. The code is deeply nested, mixes concerns and has no unit tests. Changing the code seems risky and you want to add at least characterisation tests. But even to add such a minimal safety net requires changes to the old stuff. It seems you stuck between a rock and a hard place.

By using ABAP TEST Seams one can replace test unfriendly behaviour of any statement within the same program. Just tag the code region in the domain part with a TEST-SEAM and inject other statements into this region. The injected code can alter variables, perform extra validations or do simply nothing.

For example you can:

  • replace the effect of an authority-check by setting the return code in SY-SUBRC
  • substitute a database query by populating the internal table with well known values
  • create test doubles instead of test unfriendly depended on objects
  • validate content instead of writing it to database,

Methods of test classes basically replace the code of the test seam with the code of the test injection. The code of the test injection gets executed in the runtime context of the test seam. Consequently the code of the test injection has access to variables and members visible to the domain code only. Controversially the code in the test injection has no access to variables visible to the method with the injection. If you desire to pass content from the test class to the injected code you may consider the use of global variables. Although the test seams are declared within the domain code, they do not alter the behaviour of the domain code for the productive use case. Therefore test seams have not the smell of the anti-pattern "Test code in Production".


Test seams are expected to be especially useful to:

  • get legacy code under test
  • substitute dependency that shall not get exposed

Test seams are not the first choice for:

  • integration and component tests - as test injections are restricted to the same program
  • scenarios where dependency shall get exposed by object seams for architectural reasons

Try it and judge how to benefit from this new unique test technique in ABAP.

Example Snippet: Replace Authority Check

Domain Code with SeamTest Code with Injection



test-seam authorization_Seam.


     authority-ckeck

       object 'S_CTS_ADMI'
       id 'CTS_ADMFCT'

       field 'TABL'.


end-test-seam.


if ( 0 eq sy-subrc ).

  is_Authorized = abap_True.
endif.




test-injection authorization_Seam.


  sy-subrc = 0.

           

end-test-injection.

Example Snippet: Substitute Database Query With Well Known Content

Domain Code with SeamTest Code with Injection


test-seam read_Content_Seam.


  select * from sflight

    into table @flights[]

    where

      carrid in @carrid_Range[] and

      fldate eq @sy-datum.

end-test-seam.
 


test-injection read_Content_Seam.


  flights =

    value #(

      ( carrid = 'LHA'

        connid = 100 seatsmax = 30 )

      ( carrid = 'AF3'

        connid = 7 seatsmax = 1 ) ).


end-test-injection.

Example Snippet: Inject Test Double Instead of Test Unfriendly Dependency

Domain Code with SeamTest Code with Injection

test-seam inject_Double_Seam.      


  me->f_Repository =

    new zcl_Db_Query( ).


end-test-seam.
 

test-injection inject_Double_Seam.     


  me->f_Repository =

   new th_Dummy_Repository( ).  

end-test-injection.


Example Snippet: Validate Instead of Writing to Database Table

Domain Code with SeamTest Code with Injection


test-seam inject_Validate_Seam.


  modify sflight

   from table @altered_Fligths[].     

end-test-seam.
 


test-injection inject_Validate_Seam.


  cl_Abap_Unit_Assert=>assert_Equals(

    act = altered_Flights[]

    exp =

      th_Global_Buffer=>exp_Flights[] ).


end-test-injection.


Full Example: Legacy Function Module With Test Seams

For example the  fury function module RS_AU_SAMPLE_PROPOSE_BOOKING mixes up authorisation, database access and business logic. In such a case one can embrace the authorisation and the repository logic each with a test seam as shown below.


function rs_Au_Sample_Propose_Booking
   importing
     value(i_Carr_Id) type s_Carr_Id
     value(i_Conn_Id) type s_Conn_Id
     value(i_Flight_Date) type s_Date
   exporting
     value(e_Booking_Id) type s_Book_Id
   exceptions
     not_Authorized
     invalid_Flight
     no_Free_Seats.
   data:
     is_Authorized      type abap_Bool,
     flight             type sflight,
     total_Of_Bookings  type i.
   test-seam authorization_Check.
     authority-check object 'S_CTS_ADMI' id 'CTS_ADMFCT' field 'TABL'.
     if ( 0 eq sy-subrc ).
       is_Authorized = abap_True.
     endif.
   end-test-seam.
   if ( abap_False eq is_Authorized ).
     message 'You not authorized to issue bookings'(noa) type 'E' raising not_Authorized.
   endif.
   test-seam read_From_Db.
     select single *
       from sflight into flight
       where
         carrid = i_Carr_Id         and
         connid = i_Conn_Id         and
         fldate = i_Flight_Date.
     select count( * )
       from sbook
       into (total_Of_Bookings)
       where
         carrid = i_Carr_Id       and
         connid = i_Conn_Id       and
         fldate = i_Flight_Date.
   end-test-seam.
   " validate content
   if ( flight is initial ).
     message 'Flight does not exist'(nof) type 'E' raising invalid_Flight.
   elseif ( flight-seatsocc >= flight-seatsmax ).
     message 'Limit of Bookings exceeded'(nob) type 'E' raising no_Free_Seats.
   endif.
   " propose new id
   e_Booking_Id = total_Of_Bookings + 1.
endfunction.








The test injection of test class tc_Authorization_Check alters the content of the state variable is_Authorized in with literals. Please note it is not possible to pass the parameter i_Is_Permitted directly as this variable is not known in context of the seam. The code injected into the seam has access to the same artefacts as the domain code only!


class tc_Authorization_Check definition for testing risk level harmless.
  private section.
    methods:
      pass_Permission_Check    for testing,
      raise_Missing_Permission for testing,
      set_Authorization
        importing   i_Is_Permitted    type abap_Bool,
      has_Passed_Authorization_Check
        returning   value(result)       type abap_Bool.
endclass.
class tc_Authorization_Check implementation.
  method pass_Permission_Check.
    set_Authorization( abap_True ).
    cl_Abap_Unit_Assert=>assert_True( has_Passed_Authorization_Check( ) ).
  endmethod.
  method raise_Missing_Permission.
    set_Authorization( abap_False ).
    cl_Abap_Unit_Assert=>assert_False( has_Passed_Authorization_Check( ) ).
  endmethod.
  method set_Authorization.
    " the code within the INJECT SEAM statement will substitute
    " the code within the according TEST SEAM statement in the same program
    " please not the statements within the body may only make use of variables
    " visible in the context of the seam.
    if ( abap_True eq i_Is_Permitted ).
      test-injection authorization_Check.
        is_Authorized = abap_True.
      end-test-injection.
    else.
      test-injection authorization_Check.
        is_Authorized = abap_False.
      end-test-injection.
    endif.
  endmethod.
  method has_Passed_Authorization_Check.
    call function 'RS_AU_SAMPLE_PROPOSE_BOOKING'
      exporting     i_Carr_Id =     ''
                    i_Conn_Id =     0
                    i_Flight_Date = sy-datum
      exceptions    not_Authorized = 1
                    others =         9.
    if ( 1 = sy-subrc ).
      result = abap_False.
    else.
      result = abap_True.
    endif.
  endmethod.
endclass.








The test injection of test class tc_Propose defines in the setup method a static public member (global) variable that shall be used instead of the database query. As the static public member is visible to the domain code, the injected code and the domain code direct assignments are possible.(See also test class include LRS_AU_SAMPLE_TEST_SEAMST99 / SAP_BASIS 7.50 onwards).


" The test class 'tc_Propose' makes use of the test seam 'authorization_Check'
" to bypass the authorization check and makes use of the test seam
" 'read_From_DB' to provide well known input to be business logic. Finally
" the output of the business logic gets exercised and checked.
" As local data / private members of the test class can not be used in the
" Injection the test class exposes the test data via public attributes, another
" possibility might have been public getter methods.
class tc_Propose definition for testing risk level harmless.
   private section.
     class-data:
       fg_Flight                type sflight,
       fg_Total_Of_Bookings     type i.
   methods:
     setup,
     book_Non_Existing_Flight_Fails for testing,
     book_Full_Flight_Fails for testing,
     book_Available_Flight for testing.
endclass.
class tc_Propose implementation.
  method setup.
    test-injection read_From_Db.
      flight =              th_Dummy_Repository=>flight.
      total_Of_Bookings =   th_Dummy_Repository=>total_Of_Bookings.
    end-test-injection.
    test-injection authorization_Check.
      is_Authorized = abap_True.
    end-test-injection.
    clear th_Dummy_Repository=>flight.
    clear th_Dummy_Repository=>total_Of_Bookings.
  endmethod.
  method book_Non_Existing_Flight_Fails.
    call function 'RS_AU_SAMPLE_PROPOSE_BOOKING'
      exporting     i_Carr_Id =     ''
                    i_Conn_Id =     0
                    i_Flight_Date = sy-datum
      exceptions    not_Authorized = 1
                    invalid_Flight = 2
                    no_Free_Seats =  3
                    others =         9.
     cl_Abap_Unit_Assert=>assert_Subrc( act = sy-subrc exp = 2 msg = 'no flight' ).
  endmethod.
  method book_Full_Flight_Fails.
    constants:  c_Count_Bookings type i value 10.
    th_Dummy_Repository=>flight-seatsmax = c_Count_Bookings.
    th_Dummy_Repository=>flight-seatsocc = c_Count_Bookings.
    th_Dummy_Repository=>total_Of_Bookings = c_Count_Bookings.
    call function 'RS_AU_SAMPLE_PROPOSE_BOOKING'
      exporting     i_Carr_Id =     ''
                    i_Conn_Id =     0
                    i_Flight_Date = sy-datum
      exceptions    not_Authorized = 1
                    invalid_Flight = 2
                    no_Free_Seats =  3
                    others =         9.
     cl_Abap_Unit_Assert=>assert_Subrc( act = sy-subrc exp = 3 msg = 'no seats' ).
  endmethod.
  method book_Available_Flight.
    constants:  c_Count_Bookings type i value 10.
    data:       next_Id type s_Book_Id.
    th_Dummy_Repository=>flight-seatsmax = c_Count_Bookings + 10.
    th_Dummy_Repository=>flight-seatsocc = c_Count_Bookings.
    th_Dummy_Repository=>total_Of_Bookings = c_Count_Bookings.
    call function 'RS_AU_SAMPLE_PROPOSE_BOOKING'
      exporting     i_Carr_Id =     ''
                    i_Conn_Id =     0
                    i_Flight_Date = sy-datum
      importing     e_Booking_Id = next_Id
      exceptions    not_Authorized = 1
                    invalid_Flight = 2
                    no_Free_Seats =  3
                    others =         9.
      cl_Abap_Unit_Assert=>assert_Subrc( act = sy-subrc exp = 0 msg = 'return code').
      cl_Abap_Unit_Assert=>assert_Equals(  act = next_Id exp = c_Count_Bookings + 1 msg = 'proposed id' ).
  endmethod.
endclass.








7 Comments
Former Member
0 Kudos

This is actually a realy nice thing to have. Thanks for your post. We have a brown field of legacy code and I will defnitely try this :smile: .

However we will likely limit this to real legacy code and I hope that we won't see this everywhere in our new code base. But I still hope that there will be a native dependency injection or some other kind of inversion of controll principle soon in ABAP.

rainer_winkler
Contributor
Hi Klaus,

a big thank you to you and your colleagues who made this test feature!

I write unit Tests since a few years but only in the last month did I start to make it near to always when I write coding. Without TEST-SEAM´s I was often forced to develop in a certain style, just to make unit Tests possible. This decisions made the development more complex and added costs. But it was justified as unit tests saved money. In other cases I decided to write no unit test as the effort appeared to big.

I use TEST-SEAM´s since a few weeks for existing and NEW code and it made programming with unit tests easier and more fun. I add now unit tests even to code that would have been untested before.

A very important Innovation, Thank you!

Best regards

Rainer Winkler
former_member183804
Active Contributor

Thanks 4 your positive feedback

rainer_winkler
Contributor
0 Kudos
Hi Klaus, Hi SAP Community,

do you know other computer languages that have such a feature?

I use this feature now since 7 month for new and existing coding. And it is together with the new 7.40 ABAP features (Constructor operators) the main reason that I regard ABAP to be one of the most innovative computer languages that exist currently.

Best regards

Rainer Winkler
former_member183804
Active Contributor
0 Kudos
Helllo Rainer,

as far I am know a statement like TEST-SEAM is not available for other languages. Depending on the programming language there might be comparable techniques to offer a test seam. For instance Michael Feather describes in "Working effectively with legacy code" link seams for C++.

All the best

Klaus

 
joachimrees1
Active Contributor
This seams ( pun! 🙂 ) really useful, hope I can soon try it out!
best
Joachim
rainer_winkler
Contributor
0 Kudos
Hi Joachim,

you may check the blog where I summarized my experiences with Test Seams https://blogs.sap.com/2018/06/08/abap-test-seam-for-unit-test-with-external-dependencies-personal-gu...

Good luck!

Rainer

 
Labels in this area