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:
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
– 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.
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.
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.
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.
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:
- Header data is displayed to the user.
- Item data is displayed to the user.
Negative Test Cases:
- There is no header data to be displayed to the user.
- 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.