Skip to Content

The rewrite

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.

Problem

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.

Solution

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.

Some thoughts

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.

 

To report this post you need to login first.

21 Comments

You must be Logged on to comment or reply to a post.

  1. Mike Pokraka

    Nice blog, hilights the usefulness of interfaces.

    I’m not sure if it would help in your scenario, but a little trick I’ve used a few times was to create a hidden interface for turning static methods into instance methods, precisely for the scenario of removing dependencies. It’s on my long list of blog ideas, but in short:

    If your object is instantiated by a static method cl_obj=>new, then you can create an interface with a corresponding instance method lif_obj->new. Either your current class or a new local class can implement this interface, and you move the NEW code into there.

    Inside the static NEW, you create a passthrough to a singleton lif_obj->new. In your unit test you can now inject a custom lif_obj prior to your tests.

    It’s a way of gradually converting static methods and direct references to interface instance methods without disrupting current calling code. And from a unit testing perspective the nice thing is you don’t need to know where NEW is called from, it can be several levels deep.

    (3) 
      1. Michelle Crapo

        Too true!!!!  Right now I’m moving old code to new server.  I’m trying to NOT make changes.  And finding in many of the programs I have to.

        (0) 
        1. Matthew Billingham Post author

          Put them in a nice secure test harness before changing them. Of course, to do that you have to refactor… which is changing them…

          At least you could write unit tests to cover the bits you’re having to change? Or is it all global variables and forms… 😮

          (1) 
          1. Michelle Crapo

            I am changing them.   And I am writing the unit tests after.  Some of the programs were the first ever written at this company.  However, they do need changing to work with HANA.   I’m good and terrified that the programs will break….

            Now with dates looming it is critical that I have testing done.   Although our consultants are “unit testing” and “integration” testing them.  There easily could be something missed.  These are OLD. 3.1H old with no changes needed for 4.6C.

            And as everyone knows – dates don’t change in large projects.   There are only 24 hours in a day.  (20 if I want some sleep)  🙂

            In other words we are getting to the end!  I stumbled through the data migration program. (LTMC/LTMOM).  We did have a consultant – he was learning as well.  He was and is amazing.  I am still stumbling with FIORI.  Again a consultant is here for this.   He’s very busy right now.  Hopefully at the end we get some good training.

            And all this is a lot of fun!  Of course I just got back from a 3 day vacation – so I can say it is fun!  And it’s amazing I got the three day vacation.

            I am here in the community a lot.  I’m just a ghost reading what I need and then getting out.  Of course today – I’m taking some time to read some blogs I missed.

            Have a good one!

            Michelle

            (3) 
          1. Jacques Nomssi

            then this?

            PlantUML Source:

            @startuml
            class CL_CUT {
             create( )
             obj TYPE ref to LIF_SU01 
            }
            
            note top of CL_CUT: delegates to obj->create( )
            
            interface LIF_SU01 {
              create( )
            }
            
            note right: could be an abstract class
            
            LCL_SU01 --> CL_SU01
            
            CL_CUT --> LIF_SU01
            LIF_SU01 <.. LCL_SU01 
            LIF_SU01 <.. LTD_SU01 
            note right: test double
            @enduml

             

            (1) 
              1. Jacques Nomssi

                Is this still the smart proxy pattern though?

                yes I think so. As I understand it, the intent is to control access, not to add functionality (decorator) or fit to another interface (adapter).

                 

                (0) 
                1. Justin Loranger

                  Whoa… Ok now I have a whole new path of exploration.

                  This is a really good tool to help round out technical documentation, etc.

                  Thank you for sharing.

                   

                  (0) 
  2. Nabheet Madan

    Super blog, blogs like these where we actually get to know how to have TDD done are a treasure house. So far have done it for small 1-2 fixes in support, i think it is must now shall be informed to the team also to incorporate it wherever possible not always( it is debatical)

    Thanks

    Nabheet

     

    (0) 

Leave a Reply