Mock Objects and Unit Test with Injection
My last post on unit test was how to implement abap unit test with : setup and teardown framework . ABAP Unit Test with Setup & Teardown Methods
This post is about how to implement TEST injection to mock DB access objects and achieve unit test.
Consider you have one method of a class on which you have to perform unit test. Why unit test because with unit test driven development, the code would be stable. Even if later you need to change the code depending on further customer requirement, with unit test you will get to know that with additional code, you have not break the existing working software unit. Even if you break the pre-existing code functionality, you will get to know with unit test while changing the code lines for the new requirement.
Consider you have a class having methods under test. The methods on which that you are going to perform or write unit test, having access to the DB (customizing DB reads as example) and performs some additional checks on the DB records. Unit test mostly performed on the development environment and here we need to mock the data base access. The class methods under test must use a helper class to gain the DB access and such a helper class that provides all the DB access works with single responsibility principle.
Let’s work on a simple FLIGHT model method and perform the unit test. This method just tells whether on particular date for a specific airline, seat is available or not.
The class- ZCL_GET_FLIGHT_DETAILS designed based on single responsibily principle and provides DB access. It has one method that gives DB record details.
Perfect the Helper class is ready now.
Next here is a class ZCL_FLIGHT_AVAILABILITY and having one method CHECK_AVAILABILITY under unit test. This method CHECK_AVAILABILITY needs to access the above helper class to get the DB details and then performs additional check to decide whether seat is available or not.
The class under test, creates a reference to the helper DB access class.
The class under test, must have to implement CONSTRUCTOR method to support the TEST injection. Its having a optional reference parameter which points to the helper DB class.
If the REFERENCE is supplied ( during unit test scenario) just assign it else create the object of the Helper DB class(during normal production scenario).
The method under test just class the Helper DB access class and then performs some checks on this to say seat is available or not.
The helper class must say who are its friend classes, to gain access of the protected or private methods( if any). In this case the helper class having a protected method and we have to call this helper class protected method, from the main class under test.
When you call the method ZCL_FLIGHT_AVAILABILITY->CHECK_AVAILABILITY from any report ( in production i.e not unit test), then while you have to create the object of the class ZCL_FLIGHT_AVAILABILITY then you should not pass the optional parameter to the constructor. So that it works with the helper class ZCL_GET_FLIGHT_DETAILS and DB access is performed.
Now our design is ready to work with mock object with constructor injection.
Lets create a Local Test Class of the global class ZCL_FLIGHT_AVAILABILITY . Select the Local Test Classes button.
So here we have to perform unit test on the method CHECK_AVAILABILITY in different possible scenarios.
Very first we have to create a subclass of the Helper Class ZCL_GET_FLIGHT_DETAILS
and redefine the method to send some hard coded value instead of DB access.
Now create a local unit test class . Create two test methods as we want to test two test scenarios for the method ZCL_FLIGHT_AVAILABILITY->CHECK_AVAILABILITY with two different data sets.
Declare a reference of the SUBClass of the Helper DB access class. In unit test mode, we want to run the SUBClass redefined method instead of the actual helper class method.
Implement the first test method. First create the object of the Helper Subclass and call the method: SET_GLOBALS to set some global variables ( random numbers in our cases). This is an additional method that we need to help us to assign some random values during unit testing.
Then create the object of the class under test ZCL_FLIGHT_AVAILABILITY and while calling the constructor pass the reference of the helper subclass. This is called as CONSTRUCTOR INJECTION.
Now then call the method under test ZCL_FLIGHT_AVAILABILITY->CHECK_AVAILABILITY . When this method will internal calls the method, it points to the local helper subclass not to the main helper class and makes MOCK of actual DB read.
CALL METHOD gr_db_flight->get_details
iv_carrid = iv_carrid
iv_connid = iv_connid
iv_fldate = iv_fldate
es_flight = DATA(ls_flight).
DATA(lv_available) = ls_flight–seatsmax – ls_flight–seatsocc.
IF lv_available > 0.
rv_true = abap_true.
rv_true = abap_false.
Second test method, to check with different data set.
Local Test Class – Code details
* Create a SUB class of the helper class and redefine the method * In UNIT test, the DB access to be MOCKED CLASS lcl_get_flight_details DEFINITION INHERITING FROM zcl_get_flight_details. PUBLIC SECTION. METHODS: set_globals IMPORTING iv_seatsmax TYPE sflight-seatsmax iv_seatsocc TYPE sflight-seatsocc. PROTECTED SECTION. METHODS: get_details REDEFINITION. PRIVATE SECTION. DATA: gv_seatsmax TYPE sflight-seatsmax. DATA: gv_seatsocc TYPE sflight-seatsocc. ENDCLASS. CLASS lcl_get_flight_details IMPLEMENTATION. METHOD: set_globals. gv_seatsmax = iv_seatsmax. gv_seatsocc = iv_seatsocc. ENDMETHOD. METHOD get_details. * DB access Mocked here es_flight-seatsmax = gv_seatsmax. es_flight-seatsocc = gv_seatsocc. ENDMETHOD. ENDCLASS. CLASS lcl_flight_availability DEFINITION FOR TESTING DURATION SHORT RISK LEVEL HARMLESS. PUBLIC SECTION. METHODS: test_check_availability_true FOR TESTING. METHODS: test_check_availability_false FOR TESTING. PRIVATE SECTION. DATA: lr_flight_check TYPE REF TO zcl_flight_availability. DATA: lr_db_flight TYPE REF TO lcl_get_flight_details. ENDCLASS. CLASS lcl_flight_availability IMPLEMENTATION. METHOD test_check_availability_true. CREATE OBJECT lr_db_flight. CALL METHOD lr_db_flight->set_globals EXPORTING iv_seatsmax = 500 iv_seatsocc = 400. CREATE OBJECT lr_flight_check EXPORTING ir_db_flight = lr_db_flight. CALL METHOD lr_flight_check->check_availability EXPORTING iv_carrid = 'AA' iv_connid = '0017' iv_fldate = '20141126' RECEIVING rv_true = DATA(lv_true). cl_abap_unit_assert=>assert_equals( EXPORTING act = lv_true exp = abap_true msg = 'Seats Available' ). ENDMETHOD. METHOD test_check_availability_false. CREATE OBJECT lr_db_flight. CALL METHOD lr_db_flight->set_globals EXPORTING iv_seatsmax = 500 iv_seatsocc = 500. CREATE OBJECT lr_flight_check EXPORTING ir_db_flight = lr_db_flight. CALL METHOD lr_flight_check->check_availability EXPORTING iv_carrid = 'AA' " Airline Code iv_connid = '0017' iv_fldate = '20141126' RECEIVING rv_true = DATA(lv_true). cl_abap_unit_assert=>assert_equals( EXPORTING act = lv_true exp = abap_false msg = 'Seats Not Available' ). ENDMETHOD. ENDCLASS.