Those of you who've been following my musings for a while are well aware that I do have
my gripes with ABAP OO. In order to come to grips with that, I've started to dabble with some OO-coding for simple things like using local classes in report programs selecting some data and displaying it via ALV during the year. So, nothing really fancy but luckily simple enough for me to at least get my feet wet!
Discovering the beauty of functional methods
Recently, I had the need to create some checking logic (background story
here) which I decided to do via a global class instead of a function module and to use Eclipse instead of SE80 to write the code. While developing the overall structure of the code based on some existing checking logic done in an old user-exit, I realised that I could break down the checks into small bits and pieces to actually create all of them as functional methods. The main method called from elsewhere ended up as just a string of nested IF-statements with each of them simply returning ABAP_TRUE or ABAP_FALSE. As the functional methods can also have exporting parameters, it was also possible to fill the message structure with some detailed information about the error encountered, if any. This then triggers the relevant error-message to be passed back to the calling routine.
As I knew that I would be struggling more than enough with ABAP OO itself, I didn't do it the "right way" via TDD (test driven develompment) but made the concious and pragmatic decision to first write ABAP OO code I could understand and to then try and add unit tests later - if I could figure out how to do that, especially as I realised that I'd have to do some "mocking" in order to avoid actual table selects, something which seemed to be rather confusing to me.
After I had some working code which did (mostly) what I wanted it to do, I picked the brains of a colleague who knows more about ABAP OO than me and cleaned up the code some more as far as naming and breaking methods up into smaller methods was concerned. I also replaced fields from the SYST-structure within the called methods with import parameters as I had a hunch that e.g. sy-uname or sy-sysid would be problematic for the yet to be written unit tests.
METHOD check_wb_action.
DATA: message TYPE bapiret2.
"Check objects in transport for DDIC-relevance
IF ddic_objects_included( i_transport_objects ).
"We have DDIC-objects in the transport - need to check system next
IF system_for_ddic_allowed( EXPORTING i_sysid = i_sysid
IMPORTING e_message = message ).
"We have DDIC objects in the transport and are in a system for DDIC-maintenance
"Now need to check if user is allowed
IF user_for_ddic_allowed( EXPORTING i_uname = sy-uname
IMPORTING e_message = message ).
"Now check that DDIC-system is source system of all objects in transport via TADIR
IF source_system_okay_for_ddic( EXPORTING i_transport_objects = i_transport_objects
i_sysid = i_sysid
IMPORTING e_message = message ).
"All good and transport can be saved with objects
ELSE.
"At least one object in transport has not been migrated to DDIC-system yet
APPEND message TO r_message.
ENDIF.
ELSE.
"We have DDIC-objects in the transport and are in a system allowed for DDIC maintenance
"but the user doesn't have the required authorisation.
APPEND message TO r_message.
ENDIF.
ELSE.
"We have DDIC-objects in the transport but are not in a system allowed for DDIC maintenance
APPEND message TO r_message.
ENDIF.
ELSE.
"Now check that source system of all other objects in transport fits current system
IF source_system_okay_for_object( EXPORTING i_transport_objects = i_transport_objects
i_sysid = i_sysid
CHANGING e_message = message ).
"All good and transport can be saved with objects
ELSE.
"At least one object in transport has another source-system
APPEND message TO r_message.
ENDIF.
ENDIF.
ENDMETHOD.
While working on the code, I become a bit of a fan of functional methods - and hope that I'll find more reasons to actually make good use of them!
Adding unit tests to the mix
The next step was to try and wrap my head around this unit testing thing and how to deal with things like selects from tables as I had heard and read quite a bit about that but never fully grasped what all needs to fall in place to make it work. Terms like "mocking", "interface" and "test doubles" were all a bit too abstract for my liking. So, before getting too confused by just reading up on unit tests, I decided to pick the brains of SAP Community - and am I ever happy that I did post
Trying to add ABAP unit tests to a global class but could do with some help!
It didn't take long for the first responses to get posted and as of this writing - 6 days after getting it out there - 10 answers and around 40 comments had accumulated. The
longest back-and-forth had about 25 comments alone, with especially
matthew.billingham and
frdric.girod patiently responding to my seemingly never ending questions in order to clarify various items for me. Several others chimed in as well and even though I mostly used Matt's suggestions, the other answers and comments also helped to improve my understanding. So, thanks must also go to
gabmarian,
jacques.nomssi,
mike.pokraka,
bfeeb8ed7fa64a7d95efc21f74a8c135,
dealmf! This helpfulness is one of the big assets we have in SAP Community and I'm sure that I wouldn't have the code in the shape it is now without it.
To cut a long story short - the nitty gritty details are in the Q&A thread mentioned above - here is just the list of the 16 unit tests I managed to write today and have all of them run successfully:
METHODS:
setup,
ddic_check_active FOR TESTING RAISING cx_static_check,
ddic_check_from_se11_active FOR TESTING RAISING cx_static_check,
called_from_eclipse FOR TESTING RAISING cx_static_check,
not_called_from_eclipse FOR TESTING RAISING cx_static_check,
ddic_system FOR TESTING RAISING cx_static_check,
not_a_ddic_system FOR TESTING RAISING cx_static_check,
user_okay_for_ddic FOR TESTING RAISING cx_static_check,
user_not_okay_for_ddic FOR TESTING RAISING cx_static_check,
ddic_objects_included FOR TESTING RAISING cx_static_check,
no_ddic_objects_included FOR TESTING RAISING cx_static_check,
old_ddic_src_ok FOR TESTING RAISING cx_static_check,
old_ddic_src_not_ok FOR TESTING RAISING cx_static_check,
new_ddic_src_ok FOR TESTING RAISING cx_static_check,
old_ddic_sys_not_ok FOR TESTING RAISING cx_static_check,
old_no_ddic_sys_ok FOR TESTING RAISING cx_static_check,
old_no_ddic_sys_not_ok FOR TESTING RAISING cx_static_check
I sure hope that they won't be the only unit tests I'll ever write!
Update: While poking around the unit test options in Eclipse a bit more, I discovered the coverage analyzer (CTRL+SHIFT+F11). After executing it, I tweaked some of the existing unit tests and added another one to get the overall coverage to 98.79%. Now, this is a really neat feature of unit tests!
SAT to the rescue!
Throughout this OO & unit testing adventure I had the feeling that I was missing something, like a "map" of how the actual and the test code interact with each other in order to know where I was and what was executed in which sequence. I tried to find out via debugging but that was just adding to my confusion - esp. as I tried to do it in Eclipse which turned out to be one new thing too many for me. Mulling this over some more and really wanting to peer under the hood, I turned to one of my favorite troubleshooting transactions: SAT.
In order to find out if SAT was up to the task, I set up a scheduled execution without aggregation and restrictions apart from specifying my global class ZCL_CHECK_WB_ACTION for the object name. I then executed the unit tests via Eclipse and actually did get what I think is a helpful result in SAT!
For one, I can now see the sequence of events for each of the defined unit tests and which routine gets called via the test double:
For another, it's possible to see if some actual table selects have been overlooked and still require another set of "mock logic" for a table as shown in this view from an interim stage of things when I still needed to mock access to it:
So, what seemed like a daunting task to me at first, turned into a neat learning exercise, thanks to a great combination of community and tool help. The real test will obviously come when I try to create more ABAP OO code with unit tests and I wouldn't be too suprised if there'll then be another Q&A thread from me asking for help!