Skip to Content
Technical Articles

Implementing Unit Tests for an ABAP Report Program

I welcome you all to the third blogpost of my blog series on ABAP Unit Testing and TDD. In this blog, I will walk you through unit testing for an ABAP Report. Also, I will showcase an example where I have implemented Unit Testing for a simple report program.

Here are the links to the other blogs in this blog series:

Blog No Blog Title with embedded link
1 Understanding ABAP Unit Testing Fundamentals – Overview for Beginners
2 “Test Doubles” and using OSQL Test Double Framework for ABAP Unit Tests
3 Implementing Unit Tests for an ABAP Report Program

Well, so, you have a report program in place and now there is a need to implement Unit Testing for the same. One thing is thus clear, that the CUT here is the report program for which unit tests are to be written. If you just got confused with the term ‘CUT’ I used here, I would recommend you take a glance through my first blogpost of this series I listed above. There is a glossary of all the terms used in the world of ABAP Unit Testing listed out, which would probably help.

Class vs Report, Where’s the difference?

As we all know, classes are further modularized into methods. Similarly, the concept of modularization has been adopted into reports in the form of ‘Subroutines’. But then, there’s a significant difference.

How do you make the code inside a method run? As shown in Figure 1, you place a call from your code to the desired method that is present outside your code, with the appropriate parameters. And then, your code accesses this method. Consequently, the code inside the method starts executing.

Likewise, how do you make the code inside a subroutine run? As depicted in Figure 2, you place a call from the report program to the desired subroutine, which is also inside the same program, with the appropriate parameters.

Figure 1: Calling methods of Class C1 from your code

Figure 2: Calling Subroutine SR1 from other subroutines and local classes of the report program

So, we observe that while execution of class methods, things flow inside-out and outside-in. But in contrast for execution of subroutines in a report, things just keep flowing inside within the report itself. There’s no outside here.

Thus, coming to an important inference. Let’s suppose we have an entity, external to a class, which invokes the desired methods of this class, with the appropriate parameters. Now say in this way, this entity is responsible to test the behavior of these methods. As seen in the Blog 1, this entity is nothing but the LTC. However, the same is not possible for a report, as we can by no means make the LTC invoke the subroutines directly from outside the report.

Yes! LTC is local to the class which it tests, but it is written well outside the class to be unit tested, taking advantage of the inside-outside-inside flows, which I just described.

 

My CUT is a Report Program. How do I implement Unit Tests here?

The key here is – See the flow and sail your ship through it.

What I mean by the above statement is:

Unit Testing involves creation of a test class, with UTMs. These UTMs place a call to the ‘Units’ in the CUT to be tested. It is thus evident that when the CUT is a class, UTMs place a call to the methods of this class. On similar lines, when the CUT is a report program, UTMs are supposed to place a call to the subroutines of this report program.

So how do we manage to place a call to this subroutine from the LTC? From the report itself!!

As seen in the previous section, this is a result of the things flowing within itself, in case of executing a subroutine of a report. Thus, we need to define as well as implement the LTC inside the report itself, because unlike classes, there is no ‘outside’ where the LTC can be placed.

Now our basic plot is ready as shown in Figure 3. We now know that we have to create the LTC inside our report program. And then, from the UTMs of this LTC, call the subroutines of this particular report. Let’s sail our ship through this direction!

Figure 3: Implementing unit testing for a report program

 

How do I handle UI interactions in a report while Unit Testing

User input is normally received by the program in the form of selection screen parameters. Output is displayed in reports, preferably using ALV lists and grids. Both of these are standard functionalities provided by SAP, which are already well-tested. So, these standard functionalities are not supposed to be tested in our unit tests.

What is more important here is:

  • How the values input by the user through the screen, impact the program output:
    • That is, we need to test just the behavior of the implemented processing logic, in response to various possible user inputs.
    • If I talk in terms of report events, the code written in the event START-OF-SELECTION should be the point of focus while writing unit tests.
  • Whether we are successfully able to reach the point, where the output is displayed to the user:
    • We should not actually execute the output when the unit tests are running.
    • However, the unit tests should just ensure whether that the implemented output functionality is being invoked properly or not, by supplying the appropriate parameters.
    • This can be achieved by suitably mocking the invocation to the output functionality

 

Example for practical demonstration

I will demonstrate a very simple example to understand these things better. For the example, I have created a Z report which takes in user input and displays output as an ALV grid. The input and output are listed in the Table 1.

Table 1: Input and Output for Demo Report ZREP_UT_DEMO_DBACCESS

Input Purchase Order Number

–        To display header data for Purchase Order from table EKKO

–        To display item data for Purchase Order from table EKPO

Output

ALV Grid

–        OO ALV using CL_SALV_TABLE

The execution flow of this report is depicted in Figure 4.

 

Figure 4: Flow of the demo report program ZREP_UT_DEMO_DBACCESS

Now let us see the various sections of this report one-by-one.

Data Declaration

Figure 5: Data Declaration for demo report program

For the sake of convenience and better readability, I will be displaying only some selected fields from EKKO and EKPO tables.

Lines 8-28: Types ty_purdoc_hdr and ty_purdoc_itm are defined for the header and item data respectively, to be displayed in ALV.

Lines 30-33: Internal tables and workares for header and item data.

Line 35: As I am using CL_SALV_TABLE to display the ALV, a local object lo_alv of this class is declared here.

Line 36: If the data for the purchase order number entered by user is not available, suitable error messages have to be thrown. The element lv_msg holds such error messages.

User Input

Figure 6: User Input for demo program

Selection screen parameters are used here to fetch user input into the system.

Line 38: Parameter p_ebeln is to fetch Purchase Order number from user.

Lines 40-41: Radio-buttons to select whether user wants the header data or item data to be displayed.

Subroutine Call

With all the data declarations and user inputs in place, its time to finally call our subroutine. Keeping things simple for understanding, we have just one subroutine here. The subroutine takes the user inputs for further executing its logic as shown in Figure 4.

Line 43: The call to the subroutine disp_purdoc_alv, with the user inputs.

Subroutine Definition

Let’s have a look at the processing logic of this subroutine.

Figure 7: Subroutine definition

Line 48: Query the database table EKKO for header data of the purchase order number entered by user.

Line 55: Appending the purchase order header data to the internal table lt_purdoc_hdr. Data of this internal table will be displayed in ALV.

Line 57: Query the database table EKPO for item data of the purchase order number entered by user. Inserting this data directly into internal table lt_purdoc_itm. Data of this internal table will be displayed in ALV.

Line 64: Check conditions to proceed with ALV display.

Figure 8: Subroutine definition – Fetching error messages

Line 100:  Closing of the IF in Line 64

Line 98: Fetch error message if item data not found

Line 104: Fetch error message if header data not found

Figure 9: Subroutine definition – Creating instance of ALV Class

Line 64: If user wants to display header data, check along if the object lo_alv of ALV display class defined in Line 35 is bound or not.

Line 69: If lo_alv is not instantiated, generate an instance from factory method of cl_salv_table class. This method takes the internal table whose data is to be displayed as an input. Here, lt_purdoc_hdr.

Line 77: Display the ALV Grid on the screen to the user.

Lines 79-92: For ALV display of item data. The process is totally similar to Lines 64-77 written for header data.

Let us find out the positive and negative test cases for this ‘Unit’ (Subroutine):

Positive Test Cases:

  1. Header data is displayed to the user.
  2. Item data is displayed to the user.

Negative Test Cases:

  1. There is no header data to be displayed to the user.
  2. There is no item data to be displayed to the user.

Now that we have understood the report program, it’s time to proceed with the Unit Test Class

 

Let us begin with the definition of the LTC

Figure 10 shows the class definition of the LTC.

Figure 10: LTC Definition

Line 118: Data definition for the test environment to redirect the access from the actual database tables EKKO and EKPO, to their respective test doubles.

Lines 119-123: Declaring the test fixtures.

Lines 126-129: Unit test methods, corresponding to the identified test cases above.

One important aspect here is, unlike Blogs 1 and 2, where we saw the unit testing for global classes, the UTMs here are defined in private section of LTC, instead of public. That’s probably because, the ‘Unit’ i.e. subroutine is not a public entity.

What I mean here is, in previous cases, the units were public methods. So, it makes sense for a public UTM to access these public units. However here, the unit is local or private to this particular report program only. So, it doesn’t logically make sense for any public method to access such private entities.

However, we can make a counter argument to this, by saying that this LTC is also private to, and written very well as part of this particular report program. So, it is fine even if UTMs are public and not private. Since, on similar lines we have public methods accessing private methods of the same class.

Either ways, no harm there, and the unit tests will work totally proper in both the cases. This can be a good point of discussion. I am currently not aware of any other specific differences between public and private UTMs. Insights, suggestions, feedbacks on this topic are highly welcome and appreciated.

Lines 131-132: Declaring an internal table of the type of DOC database tables. Since, EKKO and EKPO are the DB objects which we want to mock. Test data will be loaded into these internal tables in setup() method during class implementation. DOC is very well explained in Blog 2, listed at the beginning of this blog.

Now let us have a look at the implementation of the LTC

Figure 11: LTC Implementation – class_setup()

Line 140: Creating test doubles for EKKO and EKPO.

As I mentioned while talking about handling UI interactions, earlier above, we need to mock the invocation to the output functionality. In this example, the output functionality is OO ALV, and this functionality is invoked using method display() of class cl_salv_table as shown in Lines 77 and 92 of the report.

Now, this invocation is done using the object lo_alv. Thus, we get to know that lo_alv has to be mocked. That is, we need to create a mock of lo_alv in our LTC.

What is a mock? Mock is a replica of something. As you can see in Line 142 in Figure 11, we have defined an object mo_alv_mock of the same type as lo_alv (i.e. cl_salv_table). In order for any object to exist, it needs to be instantiated after definition. So in Line 144, this mock object is instantiated in the same way as lo_alv.

Line 148: lt_purdoc_hdr is passed to factory(). Since this is a mandatory parameter. There is no harm in passing this, as anyways we are not going to display any ALV using mo_alv_mock.

Line 150: Injecting this mock object to the actual object. As a result, during execution of the subroutine through the UTMs, lo_alv will already be instantiated, because mo_mock_alv was instantiated.

On lines 64 and 79 in the report, we have placed a check such that, the ALV is displayed only if lo_alv is not instantiated. But due to this injection in Line 150, lo_alv is already instantiated and thus, as per the code design, factory() and display() methods won’t execute. Thus, we achieve the goal of not displaying the ALV while unit tests are executing. We have also ensured that we reach up to the point of final output, without displaying it.

Figure 12: LTC Implementation – setup()

Lines 156-168: Defining the test data (dummy data) for the test doubles.

Lines 170-171: Inserting the test data in the test doubles.

Now, we implement the four UTMs one by one.

Figure 13: LTC Implementation – Unit Test Method 01

Line 179: Declaring a local internal table for the expected value to be asserted with the actual value.

Lines 185-187: Input data for this test case.

Line 190: Call the subroutine with these input parameters.

Lines 193-198: Since this is a positive test case, we will get the header data, and no error message will be thrown. The same is asserted here.

On similar lines, we define the other three UTMs. The features of loading expected values, setting of input parameters for subroutine, calling the subroutine and finally assertion for all the other three UTMs, are similar to the one just described above. So, I think explaining it in details will be a repetition of the above. Hence, I am just displaying the code here for the remaining UTMs.

Figure 14: LTC Implementation – Unit Test Method 02

Figure 15: LTC Implementation – Unit Test Method 03

This is a negative test case, so expected value is the error message. And consequently, we assert this error message and also that the internal table of header data lt_purdoc_hdr is empty.

Figure 16: LTC – Unit Test Method 04

Figure 17: LTC – teardown() and class_teardown()

Lines 280-281: Since we need to clear lt_purdoc_hdr and lt_purdoc_itm adter each test case (to ensure that the unit tests are independent and repeatable, thus complying with the FIRST principles discussed in Blog 1), this is done in teardown().

Line 283: Clearing the data from the test doubles. This is also done to comply with the FIRST principles for the unit test cases to be independent and repeatable.

Line 289: Destroying the test doubles after all the test cases are executed.

There can be possible variations here too. For example, instead of using CL_SALV_TABLE, many legacy codes use the function modules approach to display ALV, along with user-defined field-catalog. This does not involve any object creation unlike lo_alv. So the best approach here is to wrap this function module in a local interface in the report. This can be done by creating a local interface with a method. Then, create a local class inside the report itself, which implements this method and places the call to the function module inside this method. And later in LTC, simply mock this method with an empty implementation.

In my upcoming blogs, I will come up with other types of scenarios encountered in the world of ABAP Unit Testing.

8 Comments
You must be Logged on to comment or reply to a post.
  • A couple of points – the first important, the second not so.

    1. You have prefixed global variables in the report with L. There are not local so should not have the L prefix.
    2. You have EXPORTING in your call to the method ASSERT_EQUALS. It’s unnecessary and detracts from readability. Also, you don’t use EXPORTING in other functional methods, so why this one?

    And a third point. use the code tool  {;} in the toolbar for code examples, rather than screen shots. Screen shots can’t be copy pasted and can’t be searched.

    Now on to the main critique/comment

    Any new report program shouldn’t use FORMs. Rather, use a local class. I use a class like lcl_main and methods of that, together with any other local methods that might be needed to modularise. Since I’m always wanting to have automated tests, I create local interfaces for all calls to standard SAP code (FMs for example, or CL_SALV_TABLE), and for database actions. In this way, I can easily create test doubles for the testing.  Where a report is complex, I might well create a global class/interfaces to do the work.

    If I need to get an existing report under test, I first convert all FORMs to methods of a local class. (Not always possible if the report uses old fashioned callbacks from certain function module – but even here I replace the body of the form with a method call). I also remove dependencies by creating interfaces and classes to handle that functionality, that can be replaced by injecting test doubles

    Edit: following statement is not correct – see reply later in the thread for the correction.

    I’m not convinced that CL_OSQL_TEST_ENVIRONMENT is a good idea, except where you can’t refactor legacy code. If your program is well designed with proper separation of concerns, you shouldn’t even need it.

     

     

     

     

    • Hi Matthew,

      Thanks for your detailed feedback and comments. I highly appreciate your outlook towards the subject.

      Yes, I do agree that a report should be refactored into local classes and interfaces. In fact, if we delve a bit deeper, that’s where testable code for ABAP actually comes from!!! And from my personal opinion, even I do always encourage this practice.

      However, in this blog post, I just wanted to keep things much more simple, and wanted to emphasize more on how reports are different than classes in terms of unit testing, and so didn’t want to introduce refactoring of reports and all those practices.

      I have already planned for blogs on refactoring a report for testable code, however, that will come up a bit later in the series.

      Regarding the naming conventions, yes, I will focus more on this aspect from my next blog onwards.

      However, your mention that CL_OSQL_TEST_ENVIRONMENT is not preferable, is a deep concern. Also, you did mention that you can create local interfaces for database actions. Yes, that must be done only when the DB is not acessed directly from your code.

      However, if the code you write, is directly accessing the DB and you use local interfaces to mock the call to database (with probably a mock implementation in unit test class), then probably you are not testing whether your DB call (say, SELECT) was successful or not. You will be just skipping this statement in your test coverage. That’s one of the reasons why database doubles were introduced in ABAP with this framework. I would highly recommend you to have a look at the purpose why this was introduced in ABAP.

      Also I feel, not everytime we can use CL_SALV_TABLE into a report, as one of the biggest advantages, is also its disadvantage too.

      Thank You.
      Happy Learning!!

      Best Regards,
      Suketu Dave

      • I’ll qualify my statement, and I stand corrected – thank you.

        I put all by DB accesses into a single class (implementing an interface of course), for each update. For mocking, I use hashed tables to emulate db accesses.

        But for the DB class itself, then CL_OSQL_TEST_ENVIRONMENT is a boon for testing, as you say. Outside of such a class, I don’t think it should be used.

         

    • Thank You for sharing your blog link here. This is something I did not know to implement.

      Will go through this.

      Thank You.
      Happy Learning!!

      Best Regards,
      Suketu Dave

  • Very informative post, Thank you for that.

    Is it possible to do integration testing with these test doubles? I mean in the main scenario flow can we replace call to DB tables with test doubles?

    • Hi Karuna,

      As far as I understand, it should not be possible to use these test doubles for integration testing.
      These test doubles are part of OSQL Test Double Framework, and are created inside the OSQL Test Environment, which is specific to AUnits (i.e. ABAP Units).

      You can get more details regarding these test doubles here.

      Regards,
      Suketu Dave.