Testability vs. simple design
Testing requires decoupling
You may know I’m a proponent for simplicity. I believe that if a particular feature can be achieved with a single if
statement, then that’s all that needs to be done. I’m also a proponent of testability, that all code should be easily testable so that we can make sure that it works. As it turns out, these two don’t usually go hand in hand. (Spoiler alert: that’s not true.)
What’s wrong with this code? (Aside from the poor naming.)
Example 1: Coupled classes
If you said “coupling”, you’re onto it. (But don’t give yourself too much credit – I saw you read it from the title.) However, that makes the code simple, doesn’t it? It is clear what class is being used. It is clear what’s doing with the data, and furthermore, it is clear which is the scope of the values that Producer
generates. Really, this is as simple as it could be. And this is good!
But then, how do you test this class? Producer
is very coupled to Consumer
. This means that every time that you use Consumer
, even in unit tests, it’ll come attached to a instance of the Producer
class.
Let’s go ahead and decouple them:
Example 2: Decoupled classess
The changes in Producer
are almost none. That’s good. There is a new interface lying around. Now we can test with mocks instead of the actual Producer
class, and we have full control of the environment in which Consumer
is executed. Plus, we decreased decoupling, which means that further changes to this system are more flexible. Good, pat yourself on the back.
Did you spot the difference?
There’s a huge difference between the first and the second example. They are not functionally equivalent.
In the first one, the method always creates a new instance of the Producer
class, while in the second, the injection preserves an instance and reuses it. This could be a problem if the instance is meant to be discarded, or disposed.
Let’s make the code in a functionally equivalent approach. Since we don’t need an instance, what we need is a reference to the class, i.e., a type.
Example 3:Decoupled by types
Ok, so now the code is functionally equivalent. But the design doesn’t properly reflect that you can only pass a Producer
type, but it rather checks it at runtime. That usually is poor design. Then, you need to activate instances, which is not a big deal, but it does require strange code compared to a simple new
. And finally, it looks awful. It’s a lot of code for what was before just 3 lines.
Points of view: YAGNI
Most people will say “YAGNI”, which stands for “You ain’t gonna need it”. This is a usual principle that when applied, means that you should not over-engineer something for the sake of some feature or flexibility you’re not using. In this case, our flexibility to pass different types, since we only need one.
However, you could say that we do need that flexibility, for testing. It may not be a functional requirement, but testing may be considered a non-functional requirement, or even a development requirement in which you need to be able to test different behaviors for your classes’ dependencies.
Points of view: KISS
Another famous acronym: KISS (“Keep it simple, stupid”). Yup, you should go for the simplest one that you need.
But if you need to test, then should you go for the third option? How’s that simple? Or maybe, that just as simple as it needs to be?
Points of view: tests are still code
The point of a design is to allow code to play together and create the results that we want them too. The initial coupling that we had in example 1 was nice for a simple implementation. But if you try to test it, we come down this path of discussion.
But tests are still part of your codebase, and you need to account for them in your design. This becomes increasingly important when you actually need your tests to perform changes, to fix bugs, or to verify how something works. Really, it pays off.
An approach: Microsoft Fakes Assemblies
Let’s say that you want to keep example 1, for its simplicity and easiness of maintenance. Or maybe you can’t change it at all because it’s legacy code. How would you test it? Microsoft came up with something called Fake Assemblies. This is how you would use it to test this.
Microsoft Fakes Aseemblies allows you to control instantiation, invocation and even internal behavior of the classes of a particular assembly. You can reference any assembly, and it gives you a bunch of Shim
classes to control them.
This is completely violating encapsulation and inheritance principles. You shouldn’t be able to do this in a program design. But this is test design, right? Maybe this is forgivable?
Is it forgivable?
Nah, not really. It is understandable if you can’t use another approach. Maybe the code is not yours. Maybe changing it is risky. Maybe you don’t even have access to the code. (DateTime.Now
messing up unit tests is the best example I can think so far.)
So this is and will always be an option, but I err on the side of a good, decoupled design.
With great power, comes great responsibility. – A spider’s uncle
This kind of approach would allow you to test your code and still create a highly coupled, dependent, and even spaghetti code. You would be able to test even the worst of codes, by just breaking it into pieces and then unit testing even its private members. While this is definitely something helpful, it also could become a louse excuse to write messy code in the first place. Be wary about this power.
Let me know what you think! Do you agree with me? If you don’t, tell me why.