Over a million developers have joined DZone.

Testability != Good Design

· DevOps Zone

Discover how to optimize your DevOps workflows with our cloud-based automated testing infrastructure, brought to you in partnership with Sauce Labs

It's a funny thing, 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. There might be code that tests cannot exercise, but then why did we write it in the first place?

When we talk about testability we usually mean "hard to test". That is a whole discussion by itself, because "hard to test" is also subjective. If we follow the theme of testing 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 code is usually not factored well, and the two 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 then 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 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;

@Injectpublic void setAccount(IAccount account) {
    this.account = account;
}

bool isOverdrawn(int limit) {
    return (account.getBalance() > limit);
} 

HAS ITS 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 you’re going to test 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. You should test it.

TESTABLE CODE IS NOT INHERENTLY DESIGNED BETTER

Sometimes the changes are risky and costly. We need to balance the need for testing with the risk, and how the tools we use impact the design.

And let’s remember: this code was not “untestable”. We set a constraint to not use PowerMockito, and tried to work around it. We could easily tested that code as-is.

For years I've heard that tools like PowerMockito and Typemock Isolator, encourage bad design, because they allow to test badly designed code. It sounds bad, but it maybe a better solution than making risky changes so you can just 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.


Download “The DevOps Journey - From Waterfall to Continuous Delivery” to learn learn about the importance of integrating automated testing into the DevOps workflow, brought to you in partnership with Sauce Labs.

Topics:

Published at DZone with permission of Gil Zilberfeld, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}