A program without bugs would be an absurdity, a nonesuch. If there were a program without any bugs then the world woud cease to exist.Geoffrey James, The Zen of Programming
I attempt to manage expectation by featuring the values and limits of unit testing in the ABAP context.
A Small Challenge
Testing is complicated and I’m an engineer. […] So when engineers build something or answer a question about how to build things, we have to be sure we’re right. We have to be sure nothing is left out. It takes a lot of work.
Robert V. Binder to his son, Testing Object-Oriented Systems (TOOS)
How many different test cases can you produce that make you feel you have adequately tested a program that
- reads three integer values and
- interprets those as representing the lengths of the sides of a triangle.
- prints a message that states whether the triangle is scalene, isosceles, or equilateral.
This exercise in Binder’s TOOS was adapted from Myers’ The Art of Software Testing. If you are typical, you have done poorly on this test.
What is Unit Testing?
Unit tests are an methodology for software development. ABAP unit tests are a mature framework in the developer toolbox.
It is on one side a matter of focus: we consider user acceptance testing as a validation of the entire system behavior that could be implemented in multiple systems. We can look at integration testing to validate the interaction of separate systems, e.g. a server (master data or SQL database) and a client system. In a single system, we can validate the system behavior as seen by the end user. This usually requires process knowledge and should ideally be performed by someone else than the original developer to reduce bias. The test might understand the system architecture, but the detail of the software implementation are typically seen as a black box.
Unit testing is seen on the other hand reduce the size of the box, it is white or gray box testing, where someone with a good understanding of the system design tries to test the modules in isolation. This is typically the developer using a unit testing framework.
At the end of the day, integration or user acceptance tests could also be created using a Unit Testing framework, but they are other tools better suited for that (e.g. eCATT). We concentrate on the Unit Tests that can only be created by developers and so must not be visible outside of the develpment phase in the Software LifeCycle management.
The Continous Integration approach has the impact of moving the test to central prominence, but this is not the main topic of this discussion.
So unit testing is something developer do. Or not.
Adoption in ABAP
The ABAP Unit framework can be seen as a kind of alien in the ABAP workbench because it came late attached to the late adoption of Object Oriented technologies with the marketing explanation of helping how to make more robust code via the testing of program routines in isolation. This did not yield a positive resonance in an environment where understanding the integration with the database is required even for smoke tests.
smoke test = start the application and see if it does not break down in flames (fire and fury Ekli dosyayı görüntüle 4353
I special requirement in ABAP development is to understanding business rules and database tables good enough to be able to create a meaningful test scenario. Mere understand of ABAP syntax does not help. The SAP selling point are business processes, not development language.
On the other hand, diversification of the toolchain is driving an understanding and sometimes adoption of agile processes so the pragmatical view of the ABAP developer is allow to be positively surprised by this new technology, but only if it pays for itself. I assume that most developer have give it a try and found out it was not worth it, at least for now.
That is why I will try to manage expectation in the lines below and present use cases for Unit Testing and my opinion of the thought process required to invest in a test suite.
Misconceptions
All tests can be unit tests: No. No all. Unit Tests should be simple. Complex processes lead to complex tests. If you spend more time of fixing the test that on the fixing code, then test are not useful
Writing unit tests is hard No. Design is hard. Chance is, if your test is hard to write, then you are trying to test too much at a time because your design is not clean. Trying to write simpler unit tests force you to separate dependencies before writing the test. This is already a big change in your software.
You should create test firsts, then create the production code. While it is possible, this is hard. I have not seen it done properly in the ABAP context. This approach is called “Growing Software guided by tests”.
You should have 100% code coverage. Why? Having tests in place do not guarantee there are no errors. They help identify areas of code with missing test. That is about the only quality measure.
So Why Are You Unit Testing?
I am testing to verify that my code perform exactly as I expect it. I do not expect to be surprised by the code or test behavior. I write test as a specification: to confirm that this module does what is expected when called with the defined parameters. In this optic, unit tests are an insurance policy for later, when I will be changing the code at refactoring time. The plan is not to find errors now. If I find an error, I will add an unit test to confirm that the behavior is as expected after the fix.
This means that my strategy is to test after the code is implemented.
There is a problem with test first, i.e. creating tests before the code is implemented: it requires a lot of discipline. It is a lot harder than you might think to just implement the minimum needed to make the test pass before doing the next step. And it might be impossible in an integrated environment with a large existing code base that has to be amended to satisfy a new required behavior.
I do not expect every one tho do it, but I surely think everyone can test last.
The problem with test last is that beginner will spend a lot of effort trying to write integration tests, no unit test. Integration test are large and brittle, they break easily and must be corrected with small changes in the code base. They are difficult to support and are often abandoned because there is no benefit in constantly working on a test suite instead of fixing errors and refactoring the production code.
No Nonsense Unit Testing
Tests are validation to enhance our confidence in the behavior of the system. After runing the tests and all test pass, we feel better. Actually, the benefit of the tests is a better feeling. We get back the Joy of Coding. Complex tests do nothing to enhance our confidence.
It is better not to write tests than to write bad unit tests, so Unit Tests should be
- simple to write
- complex unit tests slow you down, they cost time to fix and at the end of the day, you won’t trust them because they fail often.
- simple to read
- The next developer will probably delete failing tests she won’t understand. If you stop trying to fix a failing test because you don’t understand it, you can delete it.
- easy to change
- test will often fail because they are wrong. They can be wrong because of an error in the fixture (test data) or in the assumption you had while writing the test.
- If you want the test to survive the first failure, it should be easy to change.
- that is one reason to advocate test with a single assert.
- fast to execute.
- Testing will be repeated often, pratically after each change. This only make sense it the tests are automated and fast.
So the main benefit of unit test I am painting her is in the automation of a lot of small even tiny tests that confirm the system behavior.
So Do They Help At All?
After a while of system stability, the unit test do not help in daily work, but the routine all allways running them help. Why, they are cheap to run… And they help identify portion of the code wo need more testing. Check the coverage indicator and decide.. Typically those would be parts of the code where automatic testing is difficult. So we tests those manually after each change..
If not, the we can create new test for a better coverage/understanding.
I tried to downport my code and after a all nighter refactoring session the unit test catched 2 issues:
- On lower Netweaver release, one cannot use RETURNING with CHANGING or EXPORTING parameter. When I changed the RETURNING parameter to CHANGING in one case, the system behavior changed: the same parameter was used as IMPORTING with reference and CHANGING, so … side-effects. i changed to IMPORTING with VALUE( ) and the error was fixed.
- In the second downporting case, I was trying to replace the CONV i( ) statement with a helper variable, but I moved the assignment of the helper variable outside of a guard clause (TRY CATCH) and I did not notice it until the unit test failed.
What is this worth? the quality of the code is not higher, but I have considerably reduced the testing time. My confidence is high. And once I trust the test, I venture in deeper refactoring to make the code simpler without slowing down too much.
That is actually the real risk: without automatic testing, the need for manual test makes testing for large software very expensive. But you already know this already?
My parser had failed to check for the closing parenthesis in an expression and I could not detect this via unit test because I did not wrote a test for this case. It was later as I was reusing the parser code that I found the error while trying to test for malformed expression.
Tests helps to validate our code against our specification, they do nothing to ensure the specification is valid.
When is it easy to do?
When you have a good specification and/or a good separation of concern. Conversely it is hard to do unit testing when you design is changing too much, e.g. at the beginning of your project. You need much more discipline to create tests in this phase.
On the other hand, creating tests is a very useful introduction to a new code base. You verify your assumptions without risk for the production code.
Examples:
Plant UML logic – more than 100 unit tests
ABAP LISP Interpreter – 350+ unit tests
The tests are small, tiny.
They are easy to write. If a test is difficult to write, it will be abandoned at first failure.
Step by Step ABAP Refactorings
Working with Tests changed my Workflow
Once you have enough unit test cases implemented,
You want to stop doing manual testing.
I started writing tests before development of the production code to remember which features are on my log.
The only reason to write bad unit tests is to learn how to write good unit test.
If you know you are going to be working on this logic next year, than save yourself some effort and invest in this code now by writing some unit test.
Even with test last ABAP unit testing requires discipline:
- one issue at a time. one tiny problem
- If you find any other problem while checking, create another tiny test for it
- write down the big changes for later, they are easier/done with more confidence when the other parts of the system are covered with tests, so
- first create a coverage with your actual understanding of the system,
- then refactor mercilessly
It is all about ROI, I do not try to create automated test for SQL (that would be integration test) because they are no good tools support for it (maybe in upcoming releases) and after I have done a deep manual testing, I assume the code won’t change often. If it does, and it is valuable, then I should create some kind of test…
Simpler Guard Clauses
METHOD restore_saved_default_task.
IF ms_save_default_task IS INITIAL.
RETURN.
ENDIF.
Use CHECK
METHOD restore_saved_default_task.
CHECK ms_save_default_task IS NOT INITIAL.
Create Guard Clauses
ls_default_task = get_default_task( ).
IF ls_default_task IS NOT INITIAL.
clear_default_task( ls_default_task ).
ENDIF.
My target:
clear_default_task( get_default_task( ) ).
Steps:
in Method clear_default_task( ) add a new guard clause:
* Importing Paramter IS_DEFAULT_TASK
METHOD clear_default_task.
CHECK is_default_task IS NOT INITIAL.
I have to do a Where-Used check on method clear_default_task( ) to confirm the design decision does not conflict with existing code.
Use CHECK in LOOP
* build edges
LOOP AT lt_nodes ASSIGNING <ls_node>.
lt_findstrings = VALUE #( ( <ls_node>-obj_name ) ).
lv_find_obj_cls = <ls_node>-obj_type.
CALL FUNCTION 'RS_EU_CROSSREF'
EXPORTING
i_find_obj_cls = lv_find_obj_cls
TABLES
i_findstrings = lt_findstrings
o_founds = lt_founds
i_scope_object_cls = lt_scope
EXCEPTIONS
OTHERS = 9.
IF sy-subrc <> 0.
CONTINUE.
ENDIF.
new
LOOP AT lt_nodes ASSIGNING <ls_node>.
lt_findstrings = VALUE #( ( <ls_node>-obj_name ) ).
lv_find_obj_cls = <ls_node>-obj_type.
CALL FUNCTION 'RS_EU_CROSSREF'
EXPORTING
i_find_obj_cls = lv_find_obj_cls
TABLES
i_findstrings = lt_findstrings
o_founds = lt_founds
i_scope_object_cls = lt_scope
EXCEPTIONS
OTHERS = 9.
CHECK sy-subrc EQ 0.
EXPORTING Parameters for Inline Declaration
DATA: lv_order TYPE trkorr,
lv_task TYPE trkorr.
call_transport_order_popup(
IMPORTING
ev_order = lv_order
ev_task = lv_task ).
to
call_transport_order_popup(
IMPORTING
ev_order = DATA(lv_order)
ev_task = DATA(lv_task) ).
Replace INITIAL LINE by VALUE Operator
APPEND INITIAL LINE TO lt_nodes ASSIGNING <ls_node>.
<ls_node>-obj_name = <tadir_ddls>-obj_name.
<ls_node>-obj_type = <tadir_ddls>-object.
First step:
APPEND VALUE #( ) TO lt_nodes ASSIGNING <ls_node>.
<ls_node>-obj_name = <tadir_ddls>-obj_name.
<ls_node>-obj_type = <tadir_ddls>-object.
Next step:
APPEND VALUE #( obj_name = <tadir_ddls>-obj_name
obj_type = <tadir_ddls>-object ) TO lt_nodes
ASSIGNING <ls_node>.
Last step: if field-symbol <ls_node> is not used anymore:
APPEND VALUE #( obj_name = <tadir_ddls>-obj_name
obj_type = <tadir_ddls>-object ) TO lt_nodes.
Expression Oriented Code
CLEAR: lt_dependency.
APPEND VALUE #( obj_name = <tadir_ddls>-obj_name
obj_type = <tadir_ddls>-object ) TO lt_nodes ASSIGNING <ls_node>.
lt_dependency = get_ddls_dependencies( <tadir_ddls>-obj_name ).
LOOP AT lt_dependency ASSIGNING <dependency>
WHERE deptyp = 'DDLS'
AND refname = <tadir_ddls>-obj_name.
new
APPEND VALUE #( obj_name = <tadir_ddls>-obj_name
obj_type = <tadir_ddls>-object ) TO lt_nodes ASSIGNING <ls_node>.
LOOP AT get_ddls_dependencies( <tadir_ddls>-obj_name )
ASSIGNING FIELD-SYMBOL(<dependency>) WHERE deptyp = 'DDLS'
AND refname = <tadir_ddls>-obj_name.
Built-in Functions
READ TABLE lt_edges WITH KEY
from-obj_name = <ls_node>-obj_name
from-obj_type = <ls_node>-obj_type
TRANSPORTING NO FIELDS.
IF sy-subrc <> 0.
new
IF NOT line_exists( lt_edges[ from-obj_name = <ls_node>-obj_name
from-obj_type = <ls_node>-obj_type ] ).
Dynamic ASSIGN
UNASSIGN <ls_approver>.
ASSIGN COMPONENT lv_comp OF STRUCTURE ls_appr TO <ls_approver>.
CHECK <ls_approver> IS ASSIGNED.
Is this a dynamic or table expression ASSIGN? if yes, I can re-write it
ASSIGN COMPONENT lv_comp OF STRUCTURE ls_appr TO <ls_approver>.
CHECK sy-subrc EQ 0.
References
ABAP debugger enhancement or how to speed up your test data creation process | SAP Blogs
- Growing Object-Oriented Software, Guided by Tests, by Steve Freeman and Nat Pryce
Okumaya devam et...