I was reading an article about property-based testing the other day. This article started with the problem that it could be difficult to test all the cases of a function and that this was cumbersome and error prone. This triggered me to think about why such a function would be difficult to test.
The switch statement
During the years I have seen a lot of code in a lot of different domains. In all that code, I’ve seen multiple switch statements or a long if-else statement based on the same value over and over again, be it enum, string or on object types.
... switch (value) { case VALUE1: doFirstThing(); break; case VALUE2: doSecondThing(); break; default: doNothingMuch(); } ...
Usually I would see some code like this, there would be quite a lot of common code for all the methods in the one class and a lot of test cases with huge initiation blocks. It would take me quite some time to read though the code and get to know what it does. It would also take a long to see what the test actually tested and what differences for another test case would be. But how can we do this a more readable and testable way?
Clean code
Enter clean code and the SOLID principle. When I see this kind of code, I see a violation of the Single Responsibility Principle (the code does more than one thing) and the Open/Closed principle (open for extension, closed for modification).
To start of with, I see a switch statement that makes the code go in different paths. This could suggest to encapsulate each of the methods called in the switch case into its own strategy class for example. These classes would implement a common interface so that they could all be called in the same way (Dependency Inversion Principle).
Public interface Do{ public void doSomething() } class DoFirstThing implements Do{ public void doSomething() { ... } } class DoSecondThing implements Do{ public void doSomething() { ... } }
These classes can be tested in isolation and will only have one reason to change (SRP). The code base for each class should be clearer to read than the original class. Also, if the code has to be extended with another case, the classes should not be altered. A new class would be created for that case with its own test class.
Factory class
But what about the original switch statement? This could be part of a larger method and still be quite difficult to read. That class would have more than one reason to change:
- the surrounding function has to change and
- and extra case has to be added.
There is a simple solution for this: a factory class. This factory class would be responsible for the creation of the different implementations of the Do interface. This would make the test cases for the original class a lot simpler as the factory could be mocked out. Also the factory could inject all the dependencies for the Do implementations into the constructors. This would make it possible to mock out all the dependencies for the classes. The factory would also have a small test class and would only have one reason to change.
public class DoFactory { public Do createDo(Enum value) { switch (value) case VALUE1: return new DoFirstThing(dep1, dep2); case VALUE2: return new DoSecondThing(dep1); ... } }
Conclusion
To conclude, there are possibilities to take switch/if-else statements out of large functions and put them into small classes. The code that gets executed because of these switch statements could be encapsulated into there own classes. Every class could get a well defined name which would make the intent clearer and easier to understand. This makes the code base to read smaller, more readable and easier to understand and test. The original code in the very first class would be reduced to this.
... doFactory.createDo(value).doSomething(); ...