ABAP Unit Tests without database dependency – DAO concept
In this blog I would like to present you technique which is used for business logic testing without database dependency. It is implemented with object oriented design. Many developers complain that they cannot write too much unit tests because their reports use database and tables content may easily change. Removing database from testing is the key factor to have successful unit tests.
Just to remind, a good unit tests:
- Always give same result.
- The order of tests is not important – each can be run independently and must work.
Let’s imagine that there is test that uses database:
- Creates new row.
- Runs business logic which reads that row.
- Checks result .
- Deletes the row at the end.
And this test can work fine. But it may not always give same result. In case if someone else will manually create row, or change/delete it during test runtime, we can have error that will interrupt our test or invalid results finally.
That is why good unit tests:
- Do not use database.
- Do not rely on network calls or files.
I think that it is really bad thing to have “randomized” test failures. It means that logic of program and test is correct, but accidentally test is failing because of environment set up or other factors. We need to eliminate this, because unit test failure must notify about defect in business logic and not in testing environment.
Technique that I present is called dependency injection. In general we need to replace database queries with something that pretends (mocks) database. We inject new object with its new behavior to the test framework, so finally we are not using database queries – that is why it is called dependency injection.
There are many ways of doing it, like using interfaces or inheritance.
I want to recommend one approach that uses inheritance, because:
- It is simple.
- It does not require separate interface creation.
- We only extend test code to pretend database, not influence the production code.
We need to make distinction between:
- Production code – business logic executed by real program in production system. It is usually global class, report or include.
- Testing code – used only for testing, never run in production. Test code cannot be put in the production code even if it is unused, so production global class should not have methods like set_customer_for_test_only( ).
There is a design template that we can use for testing database dependent logic with dependency injection. If you follow this approach, it is easy to extend production code, database queries and testing in the future.
1. Build Data Access Object (DAO) which will be single access point to database.
- Create class method get_instance( ) which returns singleton instance of object.
- Create class method set_instance( ) that makes it possible to inject mock instance if we need it.
- Each business domain should have own DAO, like ZCL_CUSTOMER_DAO, ZCL_CONTRACT_DAO, ZCL_WORK_ORDER_DAO etc. Initially we can have one DAO for report, but if there are too many queries for different tables there, complexity increases. It is better to split it logically into separate DAO units that everyone can use, so try to make DAOs domain specific and not report oriented. Keep it simple.
- Methods in DAO should suit our program need, especially for performance reasons. If our program reads table many times and require only single column values, then build method that returns table of that column values only. However if program reads table just few times, you can build method that returns full rows content and extract column in your program.
- Methods in DAO are mainly database queries, but also function/bapi/objects calls that use database internally.
- Database logic is extracted and separated from business logic.
- There is only one access point to database queries because singleton pattern is used.
DEFINITION: CLASS-DATA mo_dao_instance TYPE REF TO zcl_employee_dao. CLASS-METHODS get_instance RETURNING VALUE(ro_instance) TYPE REF TO zcl_employee_dao. IMPLEMENTATION: METHOD get_instance. IF ( mo_dao_instance IS INITIAL ). CREATE OBJECT mo_dao_instance. ENDIF. ro_instance = mo_dao_instance. ENDMETHOD.
2. Global class (production code) keeps attribute of mo_dao_instance, which is initialized in constructor.
METHOD constructor. me->mo_employee_dao = zcl_employee_dao=>get_instance( ). ENDMETHOD.
3. All database operations from production code must be delegated to DAO instance.
- Global class never runs direct queries on database inside own methods.
- Instead, all queries are delegated to dao instance, for example:
ls_employee = mo_employee_dao->get_employee_by_id( i_employee_id ). lt_employees = mo_employee_dao->get_employees_from_department( i_department ).
4. For testing scenarios, we create new class that pretends real DAO, but has predefined results for each method.
- It may be local class if we need to pretend results only for local program, or global class if we want to share it wider.
- The class extends ZCL_EMPLOYEE_DAO, inheritance is used here.
- I use _mock addition to the name to identify that this is mocking class (convention from Java development).
CLASS lcl_employee_dao_mock DEFINITION INHERITING FROM zcl_employee_dao.
- We need to redefine only methods that will be used in testing scenario.
- However if we do not redefine some method and they are used during test, the real database access will be performed so just watch out on that.
- Optionally all methods can be implemented with empty content (by default empty result returned from methods), then write implementation for methods that we need for test scenarios.
DEFINITION: METHODS get_employee_by_id FINAL REDEFINITION. METHODS get_employees_from_department FINAL REDEFINITION. FINAL REDEFINITION.
- In lcl_employee_dao_mock methods implementation we create fixed values that we assume should be returned from database. We can program conditions to have different results for different input parameters.
METHOD get_employee_by_id. DATA ls_employee TYPE zemployee_s. IF ( i_employee_id = '00001' ). rs_employee-id = '000001'. rs_employee-name = 'Adam Krawczyk'. rs_employee-age = 29. rs_employee-department = 'ERP_DEV'. rs_employee-salary = 10000. ELSEIF ( i_employee_id = '00002' ). rs_employee-id = '000002'. rs_employee-name = 'Julia Elbow'. rs_employee-age = 35. rs_employee-department = 'ERP_DEV'. rs_employee-salary = 15000. ENDIF. ENDMETHOD. "get_employee_by_id
- Implementing methods requires knowledge of database content. When I do development, I often take real database values found during debugging/manual queries and prepare test case. In this way, you show in code what can be actually expected from database, not fake but real possible values. That helps others to understand the logic as well.
- We must know possible input values and expected results. Otherwise if we do not know it, how can we be sure that our production code logic works fine? Not knowing business domain and lack of testing data cannot be excuse for not having unit tests.
6. After we have everything above set up, we can easily inject mock DAO to unit tests.
- In the class_setup method of Unit Test class which is run once before each tests are executed, insert mock DAO into real DAO:
DEFINITION. CLASS-METHODS class_setup. IMPLEMENTATION. METHOD class_setup. DATA lo_employee_dao_mock TYPE REF TO lcl_employee_dao_mock. CREATE OBJECT lo_employee_dao_mock. ZCL_EMPLOYEE_DAO=>set_instance( lo_employee_dao_mock ). ENDMETHOD.
- And that is it. Now mock dao will be used and predefined result set is returned during all tests from our own implementation in LCL_EMPLOYEE_DAO_MOCK.
- Initially I used to also set original instance of DAO in the tear_down method, which is run after all tests are finished. However this is not needed.
- ABAP specification is that singleton instance defined as in point 1, works only within one session. It means that mock DAO instance will be injected to ZCL_EMPLOYEE_DAO only during Unit Tests execution. Even if Unit Tests are lasting longer, and in the same time I will run production code in parallel from new session (like new transaction or program run F8), because this is separate session, real DAO will be used there.
Below is the summary of all described steps, showing end to end example of production code and test code.
1. Types definition used in classes.
- Let’s define type that will be used in below example.
- Structure represents basic data of employee.
- Hashed table of employees with unique ID key.
2. DAO class for employee – definition.
- Get_instance( ) and set_instance( ) create according to described template.
- 3 methods for database queries.
3. DAO class for employee – implementation.
- get_average_age is specialized method which moves logic of average calculation to database.
- get_employees_from_department method returns table of employees, that will be used for other statistics calculations.
- For test purpose, zemployee_db_table is used and we assume that it contains same columns as structure.
4. Business class – employee statistics – definition.
- Example production code which reads employee statistics: employee data, average age of all employees and average salary in specific department.
5. Business class – employee statistics – implementation.
- mo_employee_dao is initialized in constructor and this is access point to database for business logic.
- No database direct access.
- All queries to database are done through mo_employee_dao object.
- It is simple example for demo purpose. In real life logic can be more complicated, but still only single queries are used to database, then program logic is processing results.
- Average age is read directly from database through dao.
- Average salary in department is calculated by program. For demonstration purpose, DAO is returning list of employees from department, then program calculates average. In reality it would be easier to program it as well in DAO as single database query.
6. Mock DAO – definition.
- Mock DAO extends real DAO, so it has same methods available.
- All methods from real dao are redefined in this case.
- FINAL REDEFINITION points that we do not want anyone to extend lcl_employee_dao_mock class methods, but as well we could use REDEFINITION keyword only.
- In point 7 and 8 you will se different ways of implementing testing data, for demonstration purpose. In reality it is better to keep one convention in the mock DAO class.
7. Mock DAO – implementation part 1.
- One way of test data preparation.
- There is internal table that corresponds to real database table.
- In constructor of mock DAO we initialize table like we would prepare real database table before test.
- In mock DAO methods, we use internal table to find results rather than real database table.
8. Mock DAO – implementation part 2
- If we do not need to simulate full table content we can implement testing data directly in methods.
- Based on input parameters conditions, we define received results.
- It is easy to extend testing data in the future, just build own data for new input parameter designed for new test scenario.
- Sometimes we can also hardcode database values as result of method, like in case of get_average_age.
9. Unit Test class definition.
- class_setup needed – will be run once before all tests. We need to replace real DAO with mock DAO there.
- setup method will be run before each test. New fresh instance of object to test will be created.
- lo_employee_statistics is the business logic object, that we want to test.
- 3 methods tested, two of them are tested with found and not found values.
10. Unit Test class implementation – part 1.
- It is enough to replace instance in ZCL_EMPLOYEE_DAO with mock DAO instance before all tests are started.
- This is the key point of dependency injection used.
- Any further call during tests execution, by production code (example constructor in lo_employee_statistics) that tries to get instance of DAO by ZCL_EMPLOYEE_DAO=>GET_INSTANCE( ) will now get our fake prepared instance of MOCK DAO.
- It is safe to inject fake DAO as this affects only current user session that will finish after tests are executed. Any other session that calls ZCL_EMPLOYEE_DAO=>GET_INSTANCE( ) will get real DAO.
11. Unit Test class implementation – part 2.
I am attaching also text file with all code from presented example so you can use it for testing.
I hope that after reading this blog you will see how easy itis to write unit tests even for logic that requires access to database. If it looks like much code for such simple example, believe me that it is worth to spent time and create unit tests anyhow. Even and especially complex reports need it, where simple change in the future may impact behavior and non-author is not sure if he can add new line there or not. If code is well tested, there is less chance for unexpected errors. Lately there are tools that allows you to easily execute unit tests and measure code coverage but that is another story.
Keep in mind that Unit Tests that skips database by pretending it are verifying business logic but not end to end program behavior. If there is error in real DAO method, in select statement for example, our tests will not discover it. That is why end user tests are important as well. But users have less to test or less probably will discover logic bug after code was already tested on unit level. Of course it is also possible to write unit tests for DAO class itself, by inserting, reading, validating results and deleting rows for example. But I mentioned at the beginning that this are not pure unit tests, but may be helpful anyhow. Just group them in category “may have randomize fail”.
There is one more advantage of using DAO concept. If we delegate all database operations to DAO classes in our development, they can be reused by anyone. Additionally class can be tested by F8, and single methods may be executed. In this way we can check database statements (or function methods) results that are already implemented in DAO, no need to implement temporal code or thinking how to query table or execute join statement.
I recommend you to use DAO concept and always extract database logic from business layer. I strongly encourage to write unit tests whenever it is possible. – try and see long term benefits.