Great code is testable code
In the previous post I was talking about what makes great software, which according to the Head First OOA&D book that I’m following, it resumes to 3 aspects:
- Great software satisfies the customer.
- Great software is flexible.
- Great software is maintainable and reusable.
Now I would like to enhance this with something that, in my opinion, also takes a major role on this:
Great software is testable.
No matter how pretty your code looks, if you cannot properly test your program, it will be quite hard to maintain/enhance. This is a big problem for the users, just that they might not know or care, but also for you, because software and applications are constantly changing, evolving, in some cases mutating into some other thing very different from the original scope- let’s get real here. And guess who will have to do the dirty work?
So often we find ourselves in one of these situations: Either we are asked to change our own code – more desirable, but likely that we won’t remember anything of what we wrote at the time. Or we need to change someone else’s code – less desirable, highly probable. How can we be sure that we won’t break anything after implenting changes if the code cannot be tested? The traditional approach that I’ve seen over the course of 10 years is this:
- Make some insignificant test in the development environment, sometimes check that there are no dumps.
- Transport to quality environment, test again.
- Deliver to the functional consultant, he/she runs the same test.
- Release to the user. First test: runtime error.
“Of course, they were testing with X data set and we ran our tests with Y data set” will be the first excuse. And even if it’s true, the real issue is that the software is weak, not robust, it can’t be tested without real data. In other words, the program has dependencies, thus it is impossible to test.
I’ve been reading a lot about Test-Driven development, and nowadays I’m starting to feel like Neo in The Matrix: I know kung-fu. Seriously, in my experience, TDD is the way to go when it comes to delivering good, quality software.
How do you create good tests, then? Well this feels like kind of a journey, there are many things to learn. Once again, I couldn’t recommend more Paul’s book ABAP to the future, it has been my guide and also inspired me to start writing these posts.
First of all, ABAP Unit does not have to be a pure technical tool, only available to developers. You don’t need a developer key to run unit tests over an ABAP program, therefore anyone with the proper authorizations to display programs/classes should be able to run the tests. So what if we also involve our business experts and functional consultants?
Well, with Behaviour-Driven development we can accomplish this. Paul’s talked about it already back in 2013, I’m just discovering this now. BDD is all about simplification inside your test methods, so the methods labeled FOR TESTING in abap unit will have descriptions that make sense to developer, business analyst and user. You can accomplish this by setting each test method that complements the phrase “It should….”. Then inside of this method, you create 3 helper methods following the pattern “Given …. (initial condition)”, “When …. (method to test)”, “Then …. (check result)”.
This is how I refactored my test class for the ZCL_INVENTORY class:
class lcl_test_class definition deferred. "Allow access to private components within the class class zcl_inventory definition local friends lcl_test_class. class lcl_test_class definition final for testing duration short risk level harmless. private section. types: ty_guitars type standard table of zguitars with empty key. data: mo_class_under_test type ref to zcl_inventory, guitar_instance type ref to zcl_guitar, guitars type ty_guitars. guitar_to_add type ref to zcl_guitar. guitar_to_search type ref to zcl_guitar. mo_exception_raised type abap_bool. found_guitars type zcl_inventory=>guitars_tab. methods: setup, "User Acceptance tests: "IT SHOULD.................... add_guitar_to_inventory for testing, add_duplicate_and_get_error for testing, search_within_the_inventory for testing, "GIVEN .................................................. given_guitar_attribs_entered, given_initial_inventory, "WHEN .................................................. when_guitar_is_added, when_same_guitar_twice, when_guitar_is_searched, "THEN .................................................. then_inventory_has_guitar, then_exception_is_raised, then_guitar_is_found, "Other helper methods load_mockups returning value(re_guitars) type ty_guitars. endclass.
So the idea is to include “IT SHOULD” methods as they came right out of the functional specification document. In my example, The ZCL_INVENTORY class should be able to:
- Add guitars to the inventory.
- Protect the inventory against duplicate objects.
- Search for a guitar within the inventory.
Let’s see the implementation of the add_guitar_to_inventory() method:
method add_guitar_to_inventory. given_guitar_attribs_entered( ). when_guitar_is_added( ). then_inventory_has_guitar( ). endmethod.
This reads like plain english: In order to add a guitar to the inventory, we start with some guitar attributes, after we add the guitar to the inventory and check that it was succesfully included. So you run a test for this method, and if there’s no green light, you know that something is wrong with this part of the process.
The given_guitar_attribs_entered( ) method just intializes one guitar object
method given_guitar_attribs_entered. data: guitar_spec_attributes type zcl_guitar_spec=>ty_guitar_attributes. guitar_spec_attributes-builder = zcl_enum_builder=>fender. guitar_spec_attributes-model = 'Stratocaster'. guitar_spec_attributes-type = zcl_enum_guit_type=>electric. guitar_spec_attributes-backwood = zcl_enum_wood=>maple. guitar_spec_attributes-topwood = zcl_enum_wood=>maple. data(guitar_spec) = new zcl_guitar_spec( guitar_spec_attributes ). data(guitar_record) = value zcl_guitar=>ty_guitar_attributes( serialnumber = 'FE34000' price = '1745.43' specs = guitar_spec ). guitar_to_add = new zcl_guitar( guitar_record ). endmethod.
The test method itself, the one from the class under test, is part of the “WHEN…” BDD description
method when_guitar_is_added. try. mo_class_under_test->add_guitar( guitar_to_add ). catch zcx_guitar. "Oops endtry. endmethod.
Finally, we finish the process with a check in the inventory. The ABAP Unit assertions go into this part:
method then_inventory_has_guitar. data(guitar) = mo_class_under_test->guitars[ serial_number = 'FE34000' ]. cl_abap_unit_assert=>assert_not_initial( act = guitar msg = 'Guitar is not in inventory' ). endmethod.
Isn’t it nice? We ended up with a nicely written Unit Test which is meaningful to the current developer and those who will come after, to the functional consultants, business users, and even managers.
Until next time!
congrats on getting your blogs (yes, plural! J ) featured on the front page!
About unit testing: I'm familiar with the concept in theory and I do believe it's very useful. However, I have so far not gotten (or take, to be hones) a chance to use it in my work.
It always makes a lot of sense in examples, but when I get to my legacy code (with often is not even OO-based), I don't see how I could easily introduce unit tests to that.
Do you maybe have a suggestion of SAP standard code you could suggest as a positive example to look at?
(I came across Function Module PIQ_CALCULATE recently, which has some "testing stuff" with it, but I don't know if this could serve as a full blown reference - it's only two tests).
Thanks for sharing your knowledge, keep on blogging! 🙂
Actually this got me thinking: If ABAP Unit is such a good thing, then a big part of new standard code will have unit tests, right? Well, I still haven't found any hahaha
Just went to SE16N to display table SEOCLASSDF and listed all classess created after 01/01/2012, then tried to display test classes for some of the random choices I made, without success. Maybe it would help to find out what is the standard table for local test classes.
To answer to your question, most likely your legacy code is full of dependencies, making it pretty much impossible to unit test. You need to get rid of the dependencies first, which can be quite hard to do, since database access must be taken out of the main program and into it's own class, so every SELECT statement and function module call should be into one or several classes, so later on you can mock them with hardcoded values. You can do this one subroutine at a time, there's no need to change the whole program at once.
I am currently changing old legacy code in an ABAP 7.50 system. I added TEST-SEAM statements to handle the dependencies. Now I have a passing Unit Test.
After my lunch break I will implement the changes and will have a new Unit Test to demonstrate this.
This works nice
Great thx - would love to see some ABAP/framework support for BDD like cucumber
Just like Joachim, I'm also aware of the concept but have not been using it so far. It just happens that things I've been working on lately are not that complex and I also deal with lots of legacy code that we have no time (or need, really) to rewrite completely.
"Read data from these tables, put it in this format and write a file" accounts for majority of my tasks lately. It may not be representative of ABAPer majority, of course, it's just how things are where I happen to work. These simple programs don't even warrant any OOP and are easily testable without any special classes. My biggest problem is usually not lack of unit testing but someone forgetting to tell us the correct requirements.
Anyways, even if I personally don't have much use for it doesn't mean no one needs this. It's nice of you to share. What I wish though is if the authors could use the real life SAP examples instead of fictional programs in such blogs.
As you mentioned, Paul Hardy already wrote on this subject in his book and his blogs. And on Amazon some people complained that the book was using "monsters" instead of real SAP objects. But because the book is addressed to the broad audience I feel such abstraction is quite justified. (Even if Paul used, say, sales documents people would still complain why not purchase orders or accounting documents or HR records.) The SCN blogs, however, don't have the limitations of a book. So I feel it would be more beneficial to use realistic SAP scenarios. E.g. I seriously doubt anyone writes custom ABAP programs for the guitar management. (Isn't there a standard transaction for that? 🙂 )
If the blogs used real life examples then they could serve as a "bridge" between more general book knowledge and the ABAPer daily lives. I hope you consider this suggestion for the future blogs.
Still, this is much better than many blogs about "next generation disruptive digital economy innovations" posted on SCN daily. 🙂 Thank you for sharing!
So I feel it would be more beneficial to use realistic SAP scenarios.
I strongly agree with you, but....
I find it really difficult to share a real business scenario without exposing the innards of my program & hence any associated trade secrets.
Yes, that could be a problem, unfortunately. Maybe in this case it's possible at least to explain the business scenario without revealing any trade secrets? Or maybe somehow extrapolate to a standard SAP process instead of imaginary one? Or at least provide suggestions how this could be applied in SAP? Anything to bring it "closer to home" would be helpful IMHO.
It's all about finding simliarities. In my case, I'm working right now in a functional role, so all the ABAP code I'm doing is from a developer edition instance running over Amazon web services, there's no BKPF or BSEG in there, so for this reason I'm just using silly examples, one of the good things about being a contractor is having the freedom to choose the next role, so maybe I'll work as developer on the next project and can post about more realistic processes;)
However, I also believe these silly examples can be used for learning the concepts, which is the first step. Second step would be to get into a real world problem and try to apply what was learned.
Thanks for taking the time to write here!
thank you very much for your blog. Unit Tests are an important topic but not easy to apply in real projects because of dependencies.
My impression is, that the situation is different with ABAP 7.50. There TEST-SEAM´s are available. See https://blogs.sap.com/2016/02/06/working-effectively-with-abap-legacy-code-abap-test-seams-are-your-friend/ and https://blogs.sap.com/2015/10/23/abap-news-for-750-test-seams-and-injections/
I work currently with ABAP 7.50 and TEST-SEAM´s. Since then, I use Unit Test very often. Handling dependencies is much easier. This works for Legacy Code and for new code.
Thanks for sharing your experience!
PS: It is not easy to make good examples, and posting customer code is not possible. So Open Source projects can be a good source for examples. You can for instance explore the unit tests of ABAPGit. In my project (https://github.com/RainerWinkler/Moose-FAMIX-SAP-Extractor) you can also find examples how I use TEST-SEAM statements in new coding (https://github.com/RainerWinkler/Moose-FAMIX-SAP-Extractor/blob/master/src/z2mse_extract_sap2.clas.testclasses.abap).
i just created a blog post about my experience extending SAP standard code with great and testable code.