Question: Tests coupled to the implementation? White-box tests? Huge amount of work for manual dependency injection setup?
@frankdejonge thank you so much for your great content as well as for sharing with the community 🙏🙏🙏 Greatly appreciated! 🙏🙏🙏
I have a few questions about your testing approach without mocks. I would be more than happy if you could find some time to help me clarify them, if that's possible by any chance.
👉👉 1 - Let's say that we are building an e-commerce project, for example. Usually applications like that have lots of layers to be tested, in contrast to libraries.
MyCustomService001 (concrete implementation)
└── MyCustomService002 (concrete implementation)
└── MyCustomService003 (concrete implementation)
└── MyCustomService004 (concrete implementation)
└── MyCustomService005 (concrete implementation)
└── MyCustomService006 (concrete implementation)
└── *** MyCustomRepositoryInterface *** (interface) 👈👈👈
Let's say that all MyCustomServiceXXX classes don't touch external resources, like database/http/filesystem and etc... The only external hit is from MyCustomRepositoryInterface. That means I need to pass MyCustomRepositoryInterface with a concrete fake implementation MyCustomRepositoryFAKE all the way down from MyCustomService001 until MyCustomService006?
I mean, that way my test is going to touch all classes MyCustomService001 until MyCustomService006, correct?
👉👉 2 - If yes, then I would need to create aaaaaalllll dependencies manually, correct? Kinda of perform my own dependency injection for each test. For many layers, this work seems take a good amount of work... doesn't it? Especially during refactoring.
Is not there a tricky DI manager strategy to make it easier to several layers? It would be super helpful, in case you know any to share with us 👍👍👍
👉👉 3 - This once was sold me like a black-box strategy, but I was thinking to other day... this approach is actually white-box! Is not it or I'm out of my mind?
Reason: you need to know very well the implementation to make sure you create fake "ports/adapters". Also, if somebody in my team refactors the MyCustomService006 to use another two repositories, then one needs to also refactor my test... So, our test is coupled to the internal implementation... is it correct to assert that or I'm missing something?
Once again, thank you so much for your time and greatly appreciate your work 🙏🙏🙏
Hi @matheusgontijo, hope you're doing well. It's pretty hard to make a concrete out of this, but I'll do my best. There are certainly some things I have to share about this.
- When a system is hard to construct, make it easier! Dependency injection tooling is often frowned upon when used in tests, I personally do not share this opinion. At the company I currently work, we use the same DI for production as testing an provide stubbed implementation when running in test. You can also write code that builds the dependencies, which you can often re-use in your normal DI setup and in testing.
- When you have deeply nested dependencies, often there can be missing layers in between. Most problem spaces are complicated and/or complex, but not often are they super deep. There can be intermediate layers where a consumer driven contract can express the need of that particular level, which can then be stubbed out. This has the added benefit of being able to fixate the behaviour without having to reach too far down into the dependency stack.
- With regards to posts/adapters and fakes, focussing here on consumer driven contracts as well is very beneficial. The consumer knows their needs best and the interface can simplify the integration by conceptually focussing on the needs rather than the logic needed to facilitate that need. Much like tools like Flysystem do, the interface reduces the complexity of N tools into a "simple"
writecall. This is an example of interface simplification by focusing on the consumer needs. - The same consumer driven contract testing has as a benefit that refactoring should become a lot easier too, since it's all about needs, pointing down to implementation that supply a solution for that need. The simplified integration boundaries remove the friction you'd often find with deeply nested dependency graphs where implementation details leak through several layers.
I hope this helps :) and thank you for your kind words regarding the original post. I'm glad to see it keeps on being relevant.
Thank you so much for your thoughts as well as for quick reply @frankdejonge. No enough words to thank you 🙏🙏🙏