The Chilling Story of the (Inter)faceless Class.
I’ve been asked to rewrite a fairly important bit of functionality. This functionality has been modified in the past few years pretty extensively by a number of different programmers with different styles – although I wrote the initial version.
Since I’m starting from scratch, I’m naturally using the Test Driven Development approach. I mean – who wouldn’t? For various reasons, I cannot change any existing dependent classes. At least, not yet…
What I want to record is where I’ve met specific issues while developing the new version, and how I tackled them. I’m developing using Eclipse Oxygen.
The first problem I’ve got is a class that wraps user maintenance (let’s call it CL_SU01). It’s a nicely written bit of code, but with my tests, I don’t want to actually create or maintain any users. I’m working on 7.31 so I can’t use all the techniques you get on higher versions which make test doubles so much easier.
In my “code under test” class (from here referred to as my CUT-class), I create a local interface – lif_su01 with the same method definitions I need (and only those I need) from CL_SU01 , and a local class. lcl_su01 which implements lif_su01. Initially, my local interface and class are empty, except lcl_su01 has an attribute which is a instance of CL_SU01,
As I’m building my code (write test, test, write code, test, fix…) I come across (in the fix phase) a need to call a method of CL_SU01. I add that to the methods of lif_su01 and implement in lcl_su01, The method in the local classes calls the same method, which has the same signature, of my instance of CL_SU01. These are written in the Local Types tab of the CUT.class.
So, how do I make the call?
First, I create a class attribute in the private section of my CUT-class, which will be an instance of lif_su01. But lif_su01 isn’t visible to the CUT-class, so in the Class-relevant Local Types tab, I add
INTERFACE lif_su01 DEFERRED.
In the CUT-class method where I need user information, I can do something like this (I’ve included the definition of the class attribute to make it clearer.:
CLASS-DATA: _su01 TYPE REF TO lif_su01. METHOD do_something with userid. IF _su01 IS NOT BOUND. CREATE OBJECT _su01 TYPE lcl_su01 EXPORTING i_userid = i_userid. ENDIF. IF _su01->exists( )... ... ENDMETHOD.
The method exists of lcl_su01 has the same signature of as exists in CL_SU01, and it’s a simple pass-through call.
METHOD lif_su01~exists. r_exists = me->su01->exists( ). ENDMETHOD:
Now lcl_su01 isn’t visible, so in the Class-relevant Local Types tab, I add
CLASS lcl_su01 DEFINITION DEFERRED.
This bit of code is finished, but before I can test it, I need a test double.
I create class ltd_su01 in the Test Classes tab. I prefer to put my test doubles before my actual unit tests. If it gets really cumbersome, it might be an idea to put the test doubles within their own include – but I’ve found this complicates navigation and makes things less visible.
This is what my ltd_su01 class looks likes (wrt to the method exists).
CLASS ltd_su01 DEFINITION FOR TEST. PUBLIC SECTION. INTERFACES lif_su01. DATA: it_does_exist TYPE abap_bool. ENDCLASS. CLASS ltd_su01 IMPLEMENTATION. METHOD lif_su01~exists. r_exists = it_does_exist. ENDMETHOD. ENDCLASS.
Ok, now I’ve a test double, I can use it my unit test methods. I’ll put it in the setup method, as I probably don’t ever want to use the actual CL_SU01 during unit testing.
CLASS ltc_... DEFINITION FOR TESTING... PRIVATE SECTION. DATA: su01 TYPE REF TO ltd_Su01, ... METHODS: setup, test_with_existence FOR TESTING RAISING cx_static_check, test_w_o_existence FOR TESTING RAISING cx_static_check, ... ENDCLASS: METHOD setup. " Create the test double CREATE OBJECT me->su01. " Inject into my CUT-Class ZCL_CUT_CLASS=>_su01 = me->su01. ENDMETHOD. METHOD test_with_existence. me->su01->it_does_exist = abap_true. me->cut->do_something_with_userid( sy-uname ). ... ENDMETHOD. METHOD test_w_o_existence. me->su01->it_does_exist = abap_false. me->cut->do_something_with_userid( sy-uname ). ... ENDMETHOD.
When test_with_existence runs, _su01 in method do_something_with_userid will be bound already to the test double. So it will be method exists of the test double that runs. And that returns the value that I set just before I invoke do_something_with_userid.
At least. That’s the idea, but there’s another syntax failure. My test class can’t see attribute _su01 of the CUT-class.
Back to the Class-relevant Local Types tab, and add two more lines.
CLASS ltc_... DEFINITION DEFERRED. CLASS zcl_cut_class DEFINITION LOCAL FRIENDS ltc_...
Now that my test class is a friend of the CUT-class, all is syntactically correct, I can safely run my unit test in the knowledge that I’m not going to update any user accounts on the system.
To make it clearer, and with thanks to Jacques Nomssi here’s a nice UML diagram of what I did.
Another approach that might have worked, would have been to define the test double as a local subclass of CL_SU01. The problem here is that the dependent class may be marked as FINAL, or it may have a constructor that has awkward side-effects you can’t avoid, as the super->constructor must be called from the subclass constructor.
Note that it would have been simpler if CL_SU01 had been defined with reference to an interface – I wouldn’t have needed lif_su01 nor lcl_su01. It’s easy enough to extract an interface from a class, and if you use aliases for components that now have an interface~ prefix, you’re guaranteed to not break anything else that calls your dependent class.