SAP / styleguides

This repository provides SAP style guides for coding and coding-related topics.
Other
1.68k stars 446 forks source link

Use dependency inversion to inject test doubles - use constructors #246

Open cavabap opened 3 years ago

cavabap commented 3 years ago

Dependency inversion means that you hand over all dependencies to the constructor.

I feel your neglecting factory injection here. In the open sap course: writing testable code factory injection is promoted because -->it both allows for the testing single classes -->avoids complicating the constructor interfaces -->allows for integration testing where many classes are exchanged - but a few selected classes (the payment class was in their example) is exchanged. This allows the correct flow of many objects to be tested.

However I understand that factory injection is more complicated for those starting out in unit testing (but so is the ABAP test double framework)...

Thanks

fabianlupa commented 3 years ago

I think one disadvantage of using dependency lookup with factories is that your code with the dependency is now dependant on the factory. That makes it a bit more difficult to split components up, i. e. when should you have multiple factories? It is also more difficult to see which dependencies a single class has, with constructor injection they are always attributes and you get a syntax error in your test when a new dependency is added to the constructor. With dependency lookup you might by default call the real implementation by accident.

The disadvantage of constructor injection is that the handling of it is just getting really difficult when you get into having multiple dozens of interfaces with a complex dependency graph. At some point someone has to know the implementations and how to instantiate them and that point is not defined. In the worst case the user of your API has to do it. That problem does not occur with a single factory class that has all implementations at hand but for multiple factories you might run into the same situation where one factory statically relies on the other one.

Currently I am using constructor injection as the main means for dependency injection in conjunction with a dependency injection container (also called inversion of control container) that builds the dependency graph at the start of the application and lazily instantiates the classes and their dependencies as needed. That solves the problem quite well and also helps with having a defined point of entry where dependencies are analyzed / resolved that is easily and centrally maintainable without coupling technically otherwise independent components. (And yes, it's a very naive DI container implementation that analyzes constructors with RTTI and relys on SEOMETAREL for the links using a marker interface.)

As you might be able to put together from this I was using dependency lookup for everything based on that very open sap course but it simply didn't scale at all for me so I was looking for other solutions.

cavabap commented 3 years ago

I've done a number of smaller developments with dependency loop via factories and have never had a problem. I also have one much larger development with a small team - about 2 person years development using this pattern and we didn't have any such problems - I'm not saying they won't happen - but it wasn't an issue. I did find it is much better to work with in instantiated factory though - it just makes reseting the factory for the next unit test much easier.

That customer required a interface package (enforced only by convention) which contained a/some public facing interfaces. This also contained the matching public factory and class. Diving deeper into the package hierachy there was a further internal factory which managed all the internal classes. I didn't run into any problems...but that doesn't mean problems wouldn't occur. I also try to use lazy constructors, which might help in the regard.

A long time (in a Galaxy far away) I used to be a Java developer. Back then, I used to use google guice as a dependency injection container - so I'm familar with the concept. A few years I did search for and found some open source candidates (SAP blog post https://blogs.sap.com/2013/08/28/dependency-injection-for-abap/ or https://blogs.sap.com/2014/01/06/successful-abap-dependency-injection/ ) . I haven't tested them myself though.

However the same thing that stops me using ABAP test double framework , also stops me investigating an injection framework. It raises the hurdle for unit testing higher and I'd rather get people onboard than scare then away with a non SAP framework. Also I seem to remember those google guice configurations getting pretty complicted and it being hard to debug if it wasn't working. Sometimes its easier to have a plain old (factory) ABAP class that you can debug.

fabianlupa commented 3 years ago

Thanks for the response! I think what was missing for me in the open sap course was more real world complex examples, including how to use dependency lookup across different structure packages / software components. I might just need to investigate it further, since you said that works for you maybe I just haven't found the way where it scales well yet.

However the same thing that stops me using ABAP test double framework , also stops me investigating an injection framework. It raises the hurdle for unit testing higher and I'd rather get people onboard than scare then away with a non SAP framework.

I have one counter argument to this though. With dependency lookup your code under test is aware of how to get access to the dependency (because it references the factory statically). Therefore you also need to inject the mocked dependencies using the same way of access in the unit test code (with the friendly helper class). With constructor injection that is not the case, you can see immediately what you have to mock and how to supply these instances to the code under test in the constructor signature. So in that way I would argue for actually writing the unit tests it is easier if the code under test does not include the logic to retrieve the dependencies. Maybe in your case using "instantiated factories" as you said solves this problem, I am not sure.

However, when actually using these classes in production code you of course need knowledge of the framework that is then used, so at that point I do agree there is indeed some additional barrier. I also saw the available frameworks for ABAP but decided against using them since they were too complex for my use case.

cavabap commented 3 years ago

Okay. So I think our styles are quite compatible and we should do a large project together :) Let me know if you've got a large TDD project and need an external developer.

Have you considered putting your dependency injection container online as a github project and publishing it with dot abap? Its a great way of sharing your efforts, getting bug fixes and new features.

Getting back to clean code, I'd like to see factory injection also being suggested as a preffered approach.

Personally I even prefer backdoor injection even to constructor injection. Important is always - using lazy constructors.