ABAP Unit Testing: Encapsulate Database using a local Classes
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.
Nice Article Stefan.
You may not need create the stub, if you follow test double mentioned in this article ABAP Test Double Framework - An Introduction (I haven't tried it yet, though).
It would be always helpful to have a separate data selection class, so you can supply different data sources into the same object. (DB selection and/or Archive selection). And as you have pointed out through out this article, it would be helpful to implement dependency injection.
Thanks,
Naimesh Patel
Thanks for your Feedback Naimesh!
I'm really looking forward to work on a 7.4, the new Features are very interesting, I experimented with Uwe Kunath's mockA Framework and other ways of using Test Doubles.
More about that soon 🙂
Thanks & Best Regards
Stefan Mehnert
I generally use a similar approach but always declare the DB interface and the DB class globally - I don't really see any advantages in making the class local to be honest.
You can then use contructor injection (which makes the dependencies more clear) instead of having the test class change the private attribute, you'll have better SE80 support, and the DB class can be reused elsewhere. (Even if you think the DB calls are very specific to that particular class, it is still a good idea to make it resuable.)
Hello! Nice Article 🙂
I have one question that I have been wondering how to resolve; in so many cases my naturally calls standard SAP function modules that invariably make calls to the back-end database. Therefore, calling standard SAP function modules often results in it being impossible to unit test independent of the back-end.
How can I stub that behaviour? I'm thinking of actually wrapping every function module call into a class method so that I can stub the class and inject that in the unit tests, but the amount of effort to do this seems quite large.
It would be great if SAP provided an automatic stub generator for any class or function module, so that the shell could be easily created.
Do you have any thoughts on this?