Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
St-Mehnert
Explorer
7,447

Introduction

The Goals of Test Automation at XUnitPatterns.com include Keywords like "robust", "repeatable" "fully automated" and so on. In ABAP you can simply use Database Access using the OpenSQL Statements, wich maybe scattered all over your Code. Unit Testing with Database Access in General is a Problem, because you easy miss those goals.

For blackbox tests you need a Constant given State as Precondition for the Test and your Test should run as quickly as possible. For repeatable Test's this would mean that you may have to flush Tables, insert a consistent State, run your Test and finally rollback the LUW - and hope that you have replaced all dependency's that might have executed an explicit COMMIT WORK Statement. Apart from messy Setup Code you may suffer Performance Problems executing your Tests. 

Let's do one step back - what is your goal? We have to our code in several Layers, with different techniques. On the lowest level you start testing your classes in isolation, and then start to test upwards with classes in combination, a complete subsystem or End-To-End with GUI and so on.


Tesing the logic of a Class without relaying on the Database can be simply archieved using local classes. I originally found this Idea in Rüdiger Plantiko's Wiki Article ABAP Unit Best Practices. The Idea is to encapsulate all SQL statements using a local Class. Before you run a unit test you have to replace the lcl_db Instance with an Instance which does not access the database - a so called stub. The Stub returns a Structure or internal table defined by the test.


Advantages

  • Placing all the SQL statments in a local class gives you inside the class fever points of change for your database logic. Also you may reduce the number of statements, because you get a good overview of your class'es SQL Statements. 
  • You can test different execution path's of your class under test by returning different data

Disadvantages

  • To inject a local class stub instance you have to have access to the private Instance Attribute.
  • Navigating to the local class implementations using SE80 may be confusing for collegues
  • You cannot execute DB queries in your constructor if the construcor is not private (which is in general not a good idea)


This approach can also be used encapsulation Function Modules using an lcl_api class.



How to use it

Step 1: The Class under Test

In your global Class Overview you navigate to the class local definitions using the Short-Keys [CTRL]+[F5]. In this Include I define the Interface lif_db and the classes lcl_db and lcl_db_stub.


INTERFACE lif_db.

   METHODS:
     get_ztb_test_1
       IMPORTING
         i_land1 TYPE land1
       RETURNING value(rs_ztb_table_1) TYPE ztb_table_1.

ENDINTERFACE.

CLASS lcl_db DEFINITION  FINAL.

   PUBLIC SECTION.
     INTERFACES lif_db.

ENDCLASS.
CLASS lcl_db_stub DEFINITION  FINAL.

   PUBLIC SECTION.

     DATA:
       ms_ztb_table_1__to_return  TYPE ztb_table_1.

     INTERFACES lif_db.

ENDCLASS.



Then you go back the the global class definition and jump to the local class definition using [CTRL]+[SHIFT]+[F6].


CLASS lcl_db IMPLEMENTATION.

   METHOD lif_db~get_ztb_test_1.

     SELECT SINGLE *
       FROM
         ztb_table_1
       INTO
         rs_ztb_table_1
       WHERE
         land1 = i_land1.

   ENDMETHOD.

ENDCLASS.
CLASS lcl_db_stub IMPLEMENTATION.

   METHOD lif_db~get_ztb_test_1.

     rs_ztb_table_1 = me->ms_ztb_table_1__to_return.

   ENDMETHOD.

ENDCLASS.


The next Step is to add the Database Instance to your primary Class. Add an Member-Attribut in the Private Section:



mo_db TYPE REF TO lif_db.


In the Constructor you have to instantiate it.



CREATE OBJECT me->mo_db
     TYPE REF TO lcl_db.


In your Class with the production Code you can access the Database by calling the methods of mo_db.

Step 2: The Test-Class


Now let's have a look at the Test-Class. I don't use setup the generate Test Instances, normally i have a get_fcut Method, that returns the Instance to Test.


CLASS ltcl_test_my_class DEFINITION
     FOR TESTING
          DURATION SHORT
          RISK LEVEL HARMLESS
          FINAL.
PRIVATE SECTION.
METHODS:
       get_fcut
IMPORTING
           i_for_vkorg TYPE vkorg
         RETURNING VALUE(ro_fcut) TYPE REF TO zcl_encapsulated_db_access_1,
       get_db_stub
IMPORTING
           i_for_vkorg TYPE vkorg
         RETURNING value(ro_db_stub) TYPE REF TO lcl_db_stub,
       run_a_test FOR TESTING.
ENDCLASS.

Between Definition and Implementation you have to make the local Test-Class a friend of the Class under Test. That's necessary to access the private mo_db Instance and replace it with the Stub. Whenever possible you should use other techniques for Dependency-Injection.


CLASS zcl_encapsulated_db_access_1 DEFINITION LOCAL FRIENDS
       ltcl_test_my_class.

Resist the temptation the use any other "internal" private Attributes oder Methods - knowing the Internals of the Class you're testing is not a good Idea.


CLASS ltcl_test_my_class IMPLEMENTATION.
METHOD get_fcut.
CREATE OBJECT ro_fcut.
     ro_fcut->mo_db = me->get_db_stub( i_for_vkorg ).
ENDMETHOD.
METHOD get_db_stub.
CREATE OBJECT ro_db_stub.
" Setup Stub Values
     ro_db_stub->ms_ztb_table_1__to_return-vkorg = i_for_vkorg.
ENDMETHOD.
METHOD run_a_test.
ENDMETHOD
ENDCLASS.

That's it!

Resume

Building your code this way allows you the test the Logic inside the class without the Database itself. With Parameterised setup oder get_fcut Methods you can test multiple Execution Path's in your Logic. 
But be aware: Sometimes it's a narrow Path between a good Test with good Test coverage and complicated and messy Test's that get brittle overt time and complicate Changes instead as acting as a safety net.

Even if I don't unit Test the Class I tend to extract the SQL Queries in an "db" class. That allows my to hide the actual query behind an expressive Method Name.

4 Comments
Labels in this area