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
Disadvantages
This approach can also be used encapsulation Function Modules using an lcl_api class.
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.
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!
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
5 | |
4 | |
3 | |
3 | |
2 | |
2 | |
2 | |
2 | |
2 | |
2 |