It’s a funny thing, code testability. It’s not really defined, or rather, it is defined poorly.
If “testable code” is “code we can test”, that means all code is like that. We can test it through unit tests. If it’s hard we can move to functional tests. And if all fails, we can do manual testing. Even performance testing exercises the code. If there’s code that tests cannot exercise, why did we write it in the first place?
When we talk about code testability, we really mean “hard to test”. That is a whole discussion by itself, because “hard to test” is also subjective. If we follow the theme of unit testing, for example, as an investment to minimize future maintenance costs, then “hard to test” translates to “Costly to test” or “risky to test”.
So here’s another fun fact: When we have a well-factored code, it requires minimal changes, if any, for it to be tested. There’s no surprise that focusing on SRP makes code testable.
Because well-designed code is testable, we tend to correlate the two. Hard-to-test, legacy code is usually not factored well, and the two properties seem to go together. Moreover, we may infer that testable code leads to good design. It can in many cases, but not always.
Here’s an example. We have a Customer class with a static method that gets the balance of an Account from a Bank:
public bool isOverdrawn(String name, int limit) { return (Bank.getAccount(name).getBalance() > limit); }
This is a very straightforward, readable method. Its use of a static method (getAccount) makes it “untestable” in Java and other languages. Again, by “untestable” we really mean “hard to test”, which translates to “hard to mock”. In our case, using regular methods, it will be hard to mock the static method and control the input.
If we rule out use of PowerMockito, we need to modify our code to make it ”testable”. We can refactor it to pass the Account as a parameter. Once the Account is passed as an argument (really as an interface), we can mock the IAccount interface and pass it in. We now have testable code.
bool isOverdrawn(int limit, IAccount account) { return (account.getBalance() > limit); }
But has the design improved?
The method is as readable as before. We exposed the type of account, although I’m not sure we even needed to know about it. We no longer have the name parameter, but instead, we needed the caller to extract the account before the method call, while originally it did not need to bother with the Bank at all.
The design has changed, but maybe not for the better. It definitely complicated the calling code.
Now let’s try another design change for the sake of testability. This time instead of extracting a parameter, we’ll inject it with a dependency injection container (I’ll use Geuce). For that we need to modify the Customer class, and add:
private IAccount account; @Inject public void setAccount(IAccount account) { this.account = account; } bool isOverdrawn(int limit) { return (account.getBalance() > limit); }
Has the design improved now?
For the Customer class, we’ve added an unnecessary public setter, and a field we didn’t need before. If the calling code just used the setter, we’ll be in a similar condition to the last example, but using a DI framework makes the calling code, including wiring and configuration again, more complicated. (By the way, in .net it looks a bit better, but not by much.)
You may argue that sacrificing the simplicity of the calling code in order to make the design of the tested object is ok. But if we’re going to write unit tests for the calling code, you’ll need to use the same tricks, and if you’re not, well, you just made it more complex and susceptible to bugs. We should unit test it.
Testable code is not inherently designed better
Sometimes the changes are risky and costly. We need to balance the need for unit testing with the risk, and how the tools we use impact the design.
And let’s remember: this code was not “untestable”. We decided to set a constraint to not use PowerMockito, and tried to work around it. We could easily have tested that code as-is.
For years I’ve heard that tools like PowerMockito and Typemock Isolator encourage bad design, because they allow to unit test badly designed code. It sounds bad, but it maybe a better solution than making risky changes so you can just unit test. Sometimes the changes are not even risky, but will create a more complex code, where it should be simple.
Testing and design are broad skills every developer should have.
As long as you’re making a knowledgeable decision, not based on popular slogans, you’ll be fine.
Image source: https://www.flickr.com/photos/microassist/7268711202/
5 Comments
Gil Zilberfeld · September 29, 2014 at 9:54 am
Ah, you introduce domain knowledge into the story. Well played.
Note, it is not evident, but assumed 🙂 I agree that well designed code explains the domain. I have failed in this example, because of my assumptions…
Anonymous · September 29, 2014 at 9:22 am
It is evident, that the isOverDrawn is not the property of the Customer, but the Account.
What if the Customer has several accounts???
So the more testable code pointed out some discrepancy in your design, at least…
Gil Zilberfeld · September 29, 2014 at 1:15 pm
It also breaks the Law of Demeter. It’s not a SOLID code, and it still deserves to be tested.
I’ll try to make my examples clearer in the future. My intention was to explain the costs of changing the code to testability for the overall design.
Good design is in the eye of the beholder. SOLID is thought to be a good bar by many. But not always and not by everyone. Singletons may be evil to some, but are a good solution to others.
I think I’ll write on this in a separate post.
Thanks for the feedback,
Gil
Anonymous · September 29, 2014 at 12:54 pm
The “evidency” comes from the line “getAccount().getBalance()>limit”, which seems to break the simple responsibility principle, so it is not a domain knowledge.
Anonymous · October 1, 2014 at 12:42 pm
Define good design first.
If you define it as SOLID, then easily testable code is well designed.
If you define it as code which costs the least for initial development and subsequent maintenance, then easily testable code is well designed.
Code being easily testable may not be the only characteristic of good design, but it’s an extremely important one.