Unit Testing, ABAPUnit and test driven Development (TDD) have been
recent hot topics here on SAP Community.
After having taking some parts of the OpenSap Course "Writing Testable Code with ABAP" a few months ago, I recently had an opportunity to try some of it out.
My situation: I had a classic ABAP-Report: Select some stuff according to selection screen, do some calculation or transformation (in my case: sum some things up, using collect), and finally do something with the result (in my case: create an IDOC).
I think I event created it using
my template for those cases.
I had written this report with 0 UnitTests. And I probably would have head a hard time with the question "what and how exactly should I unit-Test here?!" anyway.
I had to look at the report again, when I got the following
additional requirement:
An additional mandatory parameter "company code" should be introduced.
Also, the plants, which have been parts of the selection screen already, should be verified against that company code:
Only plants that are part of this company code should be accepted (otherwise an error should be given).
-> This is, what trigger "
This are Unit-test-cases right here!" immediately.
The TDD paradigm "first thing you do: write a test" doesn’t work, as I need a least an empty structure (e.g. a class) I can test against.
[Only now, when writing this down, I realize, that I could have started with writing the test, and the create everything I use in it (classes, methods) with forward navigation. I think this was also advised in the course. ]
First thing I did (after adding parameter pa_bukrs to the selection screen) is write a class lcl_input_verification with an empty method verify_werks_against_bukrs.
(the existing coding is in lcl_report - my idea is that I probably don't have to touch that at all!)
I then went for writing a test.
First thing I noticed: there's no Test-Classes tab, as there was in the course - that's because I don't have a global class (“SE24”), but local classes (within my ABAP-Report).
But writing the test-class just underneath the other clases is ok and works fine.
It was realy helpfull that I had still access to the examples from the course, to look up things like
DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT."
and
"given
"when
"then
I created the test-class ltc_report_name with the first test-method: one_werks_belongs_to_bukrs.
As we did in the course, I wanted to start with the very verbose writing, using the given/when/then structure (beeing lazy, I create an AdT-Template for it :-))
I filled it up as follows:
"given
data(cut) = new lcl_input_verification( ).
"when
data(result) = cut->verify_werks_bukrs( iv_bukrs = '0001'
it_werks = value lcl_input_verification=>ty_werks_list(
( werks = 'W01' ) )
).
"then
cl_abap_unit_assert=>assert_true( result ) .
As you see, I didn't use a "SQL Test Double Framework" or simmilar, I just looked at V_T001K_ASSIGN in the development system, to get a valid combination.
I do know that:
- This test might break on other systems
- When customizing changes, this breaks my test.
However, and that was a recurring insigt: it was a solution available to me right at this time, and it helpt me to complete this one unit test (rather the getting lost in trying to lern yet another new thing™).
And: it doesn't prevent me from changing (improfing, refactoring) it in the future at all.
So I had my test, ran it, watched it fail.
As right out of the text book, next thing I did was: make it pass, and use the simplest way possible:
METHOD verify_werks_bukrs.
r_it_is_ok = abap true.
ENDMETHOD.
Next thing is one of those:
- write the next test
- refactor production code
- refactor test code).
I went for refectoring test code:
- Move the ' "given ' part (create object and assing its refferece to the CodeUnderTest-Varaiable (cut)) to the setup method, creating member m_cut in this process.
- Brought the rest of the test in a more 'compact' form:
METHOD setup.
"given
m_cut = new lcl_input_verification( ).
ENDMETHOD.
METHOD one_werks_belongs_to_bukrs.
"then "when
cl_abap_unit_assert=>assert_true( M_cut->VERIFY_WERKS_BUKRS( iv_bukrs = '0001'
it_werks = value lcl_input_verification=>ty_werks_list( ( werks = 'W01' ) )
)
).
ENDMETHOD.
- I also renamed the test class to ltc_input_verify , as I noticed that (only) this is what is tested here: the input-verification class.
Running my test still passes it, so there's a good chance refactoring did not break it.
I can now go ahead and write the next test, probably this one:
METHOD one_werks_belongs_NOT_to_bukrs.
"then "when
cl_abap_unit_assert=>assert_false( M_cut->VERIFY_WERKS_BUKRS( iv_bukrs = '0202'
it_werks = value lcl_input_verification=>ty_werks_list( ( werks = 'W01' ) )
)
).
ENDMETHOD.
It’s worth mentioning the “
writing” that test is now
very little effort (ya’ll know that I use AdT here, right?):
1. Highlight the first test (using mose or keyboard).
2. ctrl+alt+down.
3. Change the value behind ‘ iv_bukrs = ‘.
4. …oh and strg+1 + click to create the “definition for testing” with quick-fix.
5. Done.
Conclusion:
I went for it and did TDD - it took some time, but it gives me so much confidence for future refactorings and it will work as safety net for future changes.
It will help me finding bugs early, or - even better - not even introducing them into my code! Of course, this will save me a lot of time, compensation for my up-front investment!
*Joachim standing there, proudly showing his now-UnitTest-secured code, smiling happily and giving the 'thumps up'! *
OK, I admit it: I just copyed the textbook here!
The actual conclusion is:
I went for it and
tried something new.
As often, this
took a lot of time and I
experienced new things.
I now
know more than I have before.
Next time I add UnitTest to something, I will most likely be
faster.
I know have a
real reference (in my dev system, as opposed to the OpenSap sandbox), to which I (and maybe others) can refer to.
While I still can't answer 'yes' to "is all of your code 100% covered with UnitTest?", I can give that answer to "do you do (or did you ever)
real-life Unit-Testing?" now!
Learnings (some of those are 'no-brainers' - still I re-discover them from time to time):
- Things ‘learnd’ once (OpenSap)
quickly fade from mind, especially if not used.
- unplanned things take some time, too (e.g. Eclipse, ADT Updates..)
- it is a good idea, to follow tdd-cycle by the letter ( r_my_result = abap_true -> makes the first test pass). It helps to have
only _one_ problem/task to solve
at any given time. More might be overwhelming!)
- ABAP Unit works with
local classes, it's just not so nice. (You should
structure your code using
includes (as you should do anyway), creating a least one for test-classes).
- solving something in a not-yet-optimal way is
better than not solving it; it's the first step on the way
towards the optimal solution.
Call to action:
Go ahead, learn and try out new things!
(It's ok if you start small!).
Do talk about it, if you like!
As always, your Input is much appreciated!
Best
Joachim