ABAP News for Release 7.50 – Test Seams and Test Injections
Writing ABAP Unit tests can be somewhat cumbersome if the code to be tested is not suited for automatic tests. All the hubbub about mock frameworks or test driven development isn’t worth a cent if you have to deal with code that never came in touch with the concept of separation of concerns. Imagine you have code to maintain, that depends on database contents or calls UI screens and your boss wants you to increase the test coverage of the department – a real life scenario? Yes, at least in my life. If you cannot redesign and rewrite the whole application, as a workaround you make the code test dependent. This is regarded as bad style, but it helps.
As a simplistic example take a method that gets data from a UI screen but should be tested by a module test. Normally there is no UI available during the test. Setup and teardown methods also do not help as they might do for selecting data from a database by providing test data. A workaround before ABAP 7.50 was a free-style test flag, e.g. as follows:
CLASS cls DEFINITION.
PUBLIC SECTION.
METHODS get_input
RETURNING
VALUE(input) TYPE string.
PRIVATE SECTION.
DATA test_flag TYPE abap_bool.
ENDCLASS.
CLASS cls IMPLEMENTATION.
METHOD get_input.
IF test_flag IS INITIAL.
cl_demo_input=>request( CHANGING field = input ).
ELSE.
input = ‘xxx’.
ENDIF.
ENDMETHOD.
ENDCLASS.
The test method of a test class that is a friend of the class to be tested can influence the method by setting the test flag.
CLASS tst DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT
FINAL.
PRIVATE SECTION.
METHODS test_input FOR TESTING.
ENDCLASS.
CLASS tst IMPLEMENTATION.
METHOD test_input.
DATA(oref) = NEW cls( ).
oref->test_flag = abap_true.
DATA(input) = oref->get_input( ).
cl_abap_unit_assert=>assert_equals(
EXPORTING
exp = ‘xxx’
act = input ).
ENDMETHOD.
ENDCLASS.
Bad style and not governed by any conventions. To overcome this, with ABAP 7.50 the concept of test seams and test injections is introduced:
CLASS cls DEFINITION.
PUBLIC SECTION.
METHODS get_input
RETURNING
VALUE(input) TYPE string.
ENDCLASS.
CLASS cls IMPLEMENTATION.
METHOD get_input.
TEST-SEAM fake_input.
cl_demo_input=>request( CHANGING field = input ).
END-TEST-SEAM.
ENDMETHOD.
ENDCLASS.
With TEST-SEAM – END-TEST-SEAM a part of the code is defined as a test seam that can be replaced by test friendly code during testing. No selfdefined attribute is necessary and the test class does not have to be a friend of the class to be tested any more (as long as public methods are tested only). You don’t need an alternative implementation inside the production code because this is transferred to the test code The test method might look as follows now:
CLASS tst DEFINITION FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT
FINAL.
PRIVATE SECTION.
METHODS test_input FOR TESTING.
ENDCLASS.
CLASS tst IMPLEMENTATION.
METHOD test_input.
TEST-INJECTION fake_input.
input = ‘xxx’.
END-TEST-INJECTION.
DATA(input) = NEW cls( )->get_input( ).
cl_abap_unit_assert=>assert_equals(
EXPORTING
exp = ‘xxx’
act = input ).
ENDMETHOD.
ENDCLASS.
With TEST-INJECTION – END-TEST-INJECTIONM a test injection is defined that replaces the test seam of the same name during test execution. A test injection can be empty and then simply removes the respective test seam during testing. Test injections can be defined in test includes of global classes and function groups.
For more information, more use cases, and more examples see Test Seams.
Nice! I wonder if during implementation of the unit test you actually have access to the context of the source code being tested.
e.g. does syntax check & auto-complete work within a TEST-INJECTION?
syntax check, yes (of course)
code completion, up to now no, neither in SE80 nor in ADT
Hi Horst,
Nice! Locally, we argued a lot about the "mock frameworks". We tried local adapter classes/our custom Z*Mock/ MockA/ Test Double Framework. Nothing seems to have really bedded down and it's a real pain getting people to agree on it.
One of the issues was the amount of code that has to be put in place to encapsulate non-testable code. This is both a time issue (ie to the point where it has to be approved for build) or as you've put it - an issue with conventions (ie diff developers with different ideas on how to create the testable layer).
This may just help reduce that issue and get people over the line.
Just for consideration : I'd like to be able declare a TEST-INJECTION for a method/function and the unit test execution can dynamically make the substitution - without the application having defined a TEST-SEAM first.
Is this a possibility? I'm not sure if that dynamic approach will lead to worse code in general. As you've alluded in your blog - separation of concerns (data/logic/ui) is something that we should aspire to. But we do seem to have a lot of non-testable OO/Non-OO codebase. Taking it one step further and having the dynamic SEAM may just lead to more adoption of unit testing.
Kind Regards,
Wilbert
Hi Wilbert,
I wonder how the unit framework should know which code to replace with what if you do not instrumentalize the application explicitly.
Are you thinking in the direction of implicit test seam points, similar to the infamous implicit enhancement points? Or do you think about something like "replace lines from n1 to n2 in procedure x" (application code must be stable, then)? Or did I miss something obvious?
Best
Horst
Hi Horst,
The "dynamic test-seam" was just wishful thinking.
In order to instrumentalize code substitution, I realize that it may require fundamental changes to the runtime environment. The runtime would need to make a substitution at the point of execution. At every call, it would need to look at the definitions that has been declared in the unit test to see if the invocation can be replaced.
The "implicit team seam points" seem to satisfy what I'm thinking of. If we already have 'explicit test seam's - having the runtime recognize implicit ones around functions/methods sounds like it's not far of a jump.
While it sounds like it promotes 'bad practice' in design. It may just be the thing to make the 'good practice' of unit testing wide-spread.
By the way, any chance of a down-port to 740?
Cheers!
Wilbert
Hi Wilbert,
I discussed the idea of implicit test seam points, where only the positions after METHOD/FUNCTION and before ENDMETHOD/ENDFUNCTION make sense, but the result is, that it is not worth the effort. Since Seams and Injections are located in the same compilation unit anyway, it is not too big an effort to declare the seams explicitely.
Regards
Horst
Thank you for the response Horst. It makes sense.
More power to you and SAP for promoting the Unit Testing practice.
Cheers,
Wilbert
Nice concept - similar in spirit to extension-points.
The only point that I don't like is the naming of the new keywords. A keyword "TEST-SEAM" in productive code feels like a pollution, almost like having a test mode flag in production code.
On the other side, these test seams are only necessary for code which is dirty anyway, since it doesn't properly separate the UI or database layer from its logic layer. So it doesn't matter in the end.
Yes, but only almost. The benfit is that you get rid of the alternative implementation inside the production code. At least that is transferred to the test code.
Nice post, b.t.w.
I thought you're the ABAP boss (at least for me). #justkidding 😉
Anyway i have to admit that with each release ABAP is getting cooler 😎
Just hope that the real ABAP Boss(es) don't see that ...
This is crazy! And not at all in a good way.
Writing test code in with your production code is just pollution and I will make sure that my development team never uses this functionality.
If you can't figure out how to achieve the same with mocks, stubs, test doubles, etc then you have some seriously bad code that needs refactoring.
Building in ways for people to avoid writing good testable code is not the way to improve the ABAP language. Tests shouldn't care about the details of HOW something is implemented... Let alone be PART of that implementation!
Horst has already made it clear at the very beginning of the blog 😉
If you need to maintain legacy code without proper SoC, i think this construct is really helpful. If you are building a new application that's a separate story altogether!
The only reason to go write tests for legacy applications is if you intend to make changes to it. Otherwise you're writing tests for fun and wasting your time.
If you're already making changes to the program then you can figure out a way to build real 'test seams' and not use these features. Michael Feathers defines test seams as "a place where you can alter behaviour in your program without editing in that place" in his book Working Effectively with Legacy Code.
I would suggest that anyone who thinks these new features are a good idea go read that book...
A directive to increase test coverage can be another reason. Yes, that normally results in smoke tests that in most cases do not have any functional sense. But a full test coverage might at least help to detect dead code that never can be used because its execution leads to exceptions. And, hey, even smoke tests might reveal one or the other error in old code you haven´t been aware of.
For example TEST SEAMS do a great job to get totally untested legacy code under test initially. After the initial tests have been established one has the option to apply further reworks / refactorings.
The use of TEST SEAMS as step stone towards clean code, seems to me as working effectively with legacy code.
See also
Working effectively with ABAP legacy code - ABAP Test Seams are your friend
Hello Horst,
from the documentation:
I am correct to assume it should read Test injections instead?
We can already inject seams with by method redefinition, but I would gladly use a TEST-SEAM to isolate OpenSQL statements in unit tests. But... the limit to global classes and function pools is a hindrance for those like me who favor local classes..
best regards,
JNN
Hmm, the translation to English changed the original sentence a bit. A better translation might be "Testseams do not influence the productive usage of programs". Nevertheless also the above sentence is OK, because test seams are ignored by the compiler in production systems.
Unfortunately, test injections are possible in test includes only. And test includes are possible for class pools and function groups only. For other program types there are technical restrictions. (pls. don't ask me why ...)
Horst
Hello Jacques,
ABAP TEST-SEAMS are available only for programs with dedicated test includes, that is class.pools and function-pools. Within these program types one can inject into any executable location, including methods of local classes or even forms.
The package SABP_UNIT_SAMPLE contains the class-pool CL_AU_SAMPLE_TEST_SEAMS which showcases the use of TEST SEAMS in combination with local classes.
Hope this clarifies
Klaus
See also
Working effectively with ABAP legacy code - ABAP Test Seams are your friend
I think this can be useful but it should be emphasized that the primary (or sole) use of the test seams is to make legacy code testable - using it for new code seems to be bad practice.
Hi Gabor,
I use TEST-SEAM from time to time in new code. It depends on the risk that comes with the code and whether adding a mock object is to me worth the effort.
Suppose I need a statement authority check in a new method. Why not replacing this statement using TEST-SEAM´s with a simple line to inject SY-SUBRC = 0 ?
Code framed with TEST-SEAM is not counted in the test coverage. So if I have an application where I require near to 100% test coverage I will likely use near to no TEST-SEAM´s.
But not all coding is so complex and critical. And for such coding TEST-SEAM´s give new options to write cost efficient unit tests.
Cheers
Rainer
I quite agree this has no place in any new programs. The debate seems to be - does this technique have any place anywhere?
From my understanding the Test Seam concept is intended to be a quick and dirty workaround to add unit tests to "legacy" code (Michael Feathers defines legacy code as code with no unit tests, so if you wrote an OO program in ABAP 7.50 yesterday with no tests you have written legacy code).
In my experience the sort of code where database reads and UI logic and all the other dependencies are all mixed together like a scrambled egg tend to be executable programs and DYNPRO module pools. You cannot use test seams in either of those, so if you want to bring that code under test then doing things properly (breaking the dependencies into classes) is the only way forward, probably a good thing too.
If a global class has the same problem - all different dependencies glued together with the business logic - then OH DEAR whoever wrote the class did not have much grasp of OO principles, it is probably a function module or executable program mutated into a class with all the procedures changed to methods with no further changes, everything probably static, thus totally missing the point. Sadly I have seen quite a few global classes like that, even in standard SAP.
So, in the areas where you can use test seams e.g. a function module, the question is - which is more effort? To do things the correct way and split each sort of dependency, the database access (or whatever) into it's own class so it can be subclassed for unit tests, or to surround each such database access with a test seam and put in a test injection in the unit tests.
I think the answer is fairly clear for anything non-trivial. I'm with Lucas on this.
Cheersy Cheers
Paul
Global all-static classes are easier to convert into testable code than old spaghetti programs so I wouldn't use it there. If you have a global class with static methods, you could just delegate the calls to an instance which can then be made testable (probably not by breaking the dependencies into classes right away, but with the "subclass and override" pattern).
I can't help but think that Test Seams have been introduced to help SAP itself implement (some kind of) testing around those enormously large function groups that underpin the old but widely used transactions in ECC. I'm thinking about quality / maintenance notification, maintenance order, production order / maintenance order / network confirmations etc - the ones you wouldn't touch until you had some means of testing in place.
I think this is very likely, and the only valid use case of the test-seam keyword in my opinion.
I was just re-reading Michael Feathers book on working with legacy code, and he does indeed talk about "test seams" in the context of procedural programming languages.I cannot help but wonder if some SAP developer read that and thought "OK we will do a version of this in ABAP".
However the ABAP Test Seam concept appears to be a horrible travesty of what he (MF) was talking about.
In all the examples in the book the production code always remained totally unchanged, which is not the case with ABAP test seams. Michael Feathers was talking about procedural languages which either had some sort of pre-processing step before compilation, or a post-processing step after compilation. Those were the places he made his substitutions, not in the production code itself. Both concepts are meaningless in ABAP..
As someone somewhere once said
"We Don't Need No ABAP Test Seams,
We Don't Need No Ill Thought-Out Controls,
We Don't Need No TEST-INJECTION,
SAP leave that production code alone!
Hey! SAP!
Leave that Production Code Alone!"
Hi Paul Hardy,
I use Test Seams from time to time. And when I do it, I feel mostly happy about it.
But I did not replace the other techniques for handling during Unit Tests. My favorite technique is the pattern you described in your book “ABAP to the Future”. And I thank you a lot for teaching it to me. After reading your book I was able to use Unit Tests in real applications with calls to the database or to external functions.
But from time to time, I need the option to write a Unit Test to a class without creating further classes and testing dummies that inherit from this. Because there are cases when this effort adds complexity that that appears to me not needed. Not always do I need 100% code coverage. Not always do I need a “perfect” code or a “perfect” test.
Examples where it makes sense to me:
Examples where it does not make sense to me:
So to say, there are cases when a simpler solution is to me sufficient. And Test Seams allow me to write Unit Tests for this simpler solutions also. I know of no other language where this is as easy as in ABAP 7.50.
It appears to me, that there is a tendency to create a new dogma “Do not use Test Seams”. Not after a long discussion and evaluation of the experiences of different projects. But by something, that appear to me, to be first thoughts on it. Or a discussing from a theoretical point of view.
By the way Test Seams are a new feature I know of no other programming language. Please correct me if I am wrong with this. So we really have near to NO experience with statements like this in projects.
There are so many different types of customers, codes, situations, … in the real world. There is no simple truth. No way of programming “good”. Only a constant drive to get the best possible product under the given circumstances.
I love reading the ABAP programming guidelines. Here the authors discuss each statement or pattern and give arguments pro and contra. And such a chapter will be needed for Test Seams also.
As someone else once said at another place:
“We Need our ABAP Test Seams,
We Need this Well Thought-Out Controls,
We Need our TEST-INJECTIONs,
Not Always, but from Time to Time,
SAP please help us to code better!
And Make Us Wise To Use Tools Well.
Hey! SAP!
Please help us to code better!
Make Us Wise To Use Tools Well”
Best Regards,
Rainer Winkler
A "Test Seam" in other languages is a concept rather than an explicit command as in the ABAP implementation.
As Lucas recommends above reading the appropriate part of the Michael Feathers book would give a very good understanding of what a test seam is, as a general idea, irrespective of the implementation details in any given language. On that grounds I do not feel this idea is new, rather is that ABAP has only recently implemented this idea, based upon the general concept, and I think totally missed the point.
In regard to custom code which explicitly logs when it is being run, using more custom code, there are a fair few standard tools in the SAP system which can keep track of this sort of thing for you e.g. UPL.
Re-reading all the above posts, including the original one by Horst, it is very clear that the usage of TEST-SEAMS is a last resort, a workaround, for when you cannot do things "properly" for whatever reason.
Usually that reason is fear.
As an example you have a SELECT statement of some sort in a monstrous mass of a function group. You could create a database access class, and a method which does that same SELECT, or you could add a TEST-SEAM.
The TEST-SEAM is deemed as less effort and less dangerous. But is it really? Some would say that ANY change to existing code carries a risk.
You might think I am just having a knee-jerk reaction and have not thought this through at all, but in actual fact I have to work with several monstrous programs full of procedural code, on a daily basis. Maybe I have just been lucky, but thus far I have never had any problems isolating a single SELECT statement in its' own method - providing that is ALL I do, one tiny change at a time, just related to the small bit of the program I am enhancing/fixing.
In the book by Martin Fowler "Refactoring " Improving the design of Existing Code" he constantly stresses making one tiny change at a time.
The "theoretical point of view" often comes from people quoting from such books by Bob Martin, Martin Fowler, the Gang of Four, and so on and so forth,I had never even heard of any of them for the first 12 years I was programming in SAP. I then started reading those books after the event and was amazed how practical the advice within them has proven to be in real life. That should not be a surprise of all of them gained their experience from a multitude of real projects in real life.
Just to summarize once again, there is no need for me to argue why you should not change production code just for the sake of tests, when so many other people have argued so long and so hard against it for so very long. I'll leave it up to you to read their arguments......
Cheersy Cheers
Paul
Hi Horst,
it is now 9 month since my last comment to this blog and I would like to give an update how I see this topic currently after continuously using the statement in my projects.
The most important test for me is an automatic integration test. In most cases such a test uses more than a single class, so Test Seams are not an option.
When I write a test of a single class or a single method of a class, I do not do this to prove the correctness of an application. I do it for a sub component of an application to:
Here I use Test Seams when this fits best to my need. And in MY projects this was mostly the case.
When I use Test Seams, I use two patterns to prevent errors due to confusion:
I see no problems with Test Seam statements in productive coding. Because:
Best regards
Rainer
Hi, all! I'd appreciate your help on an issue we're facing in productive system related to a command that is between a 'TEST-SEAM' statement.
I saw the documentation and comments:
Hmm, the translation to English changed the original sentence a bit. A better translation might be “Testseams do not influence the productive usage of programs”. Nevertheless also the above sentence is OK, because test seams are ignored by the compiler in production systems.
But when I perform an SAT trace in productive system for a scenario that has a low performance, SAT indicates me that the most expensive statement is a SELECT command that is between a TEST-SEAM statement! How is that possible, given that these commands should be 'ignored' in production systems?
Am I missing something?
Thank you very much!!