Roy Osherove

View Original

Testable Designs - Round 2: Tooling, Design Smells and Bad Analogies

Eli Lopian, the guy who runs TypeMock.NET(An amazing Mocking product, BTW - a bit too powerful for it's own good, perhaps) and myself share what seem to be conflicting views on how "Testable" object Oriented applications should be out of the box  that is, should or shouldn't you be considering Testability when designing your software, and how much would you let Testability considerations change your design (add interfaces, exposes some methods, allow polymorphism etc..).

 

Eli: Testable designs are harder to read and maintain

From Eli:

"We all have have seen testable code. The code is hard to maintain and understand. A Testable code, will have many interfaces that have no real design value, that will just confuse developers. Have you ever tried to browse a code with loads of interfaces, it take ages because you have to keep finding the concrete implementation, and the place where the instance was created"

...

"Using TypeMock.NET your code can be encapsulated (OOD) and Testable at the same time"

Eli says: "Tooling can save you from testable designs"

You know, it's quite ironic. On one hand, Eli's claim is that with TypeMock, you get the automated tooling support you need to check a system without changing it's design. Which means you no longer have to "design for testability", just design, and later just use TypeMock to check it. Eli is not wrong - TypeMock is a powerful product that would allow such scenarios. Scary, isn't it? :)

I Say: Tooling can save you from un-testable designs too!

But then Eli claims that one of the side effects of Testable designs is many interfaces which make the code harder to browse and thus maintain.  Ironically, this same problem can also be solved by tooling today, using Addins for Visual Studio like Resharper, that allow navigating and finding an interface or a method's real implementations with a single keyboard shortcut or mouse click. (I admit that without Resharper, or tools like Eclipse or IntelliJ for Java, browsing a highly decoupled and Object Oriented code base can be a daunting task at first).

Eli: When interfaces and objects share the exact same contract - it's a design smell

So assuming that the "browsing code is harder" problem goes away with the correct tooling inside your favorite editor, is there still  a problem with testable designs ? Eli thinks there is because they create bad interfaces:

"Think about this code smell: When you find that an interface and implementation have exactly the same public methods, it is a sign that the interface is not exposing a trait of the object but the Object itself. This is what happens when your code is Designed for Testability, you do have a safety net, but the design is flawed."

To begin with, when you think about testability, adding an interface-per-object is one way to go, but not the only one. Adding interfaces which represent "traits" of objects is a design decision. As long as an object can be replaced by sending in something that mimics the same public contract as that object, you're good. In fact, it's not a bad thing for an object to expose multiple interfaces, each only doing small things: an object can be both ISerializable and IList, it can be both IPerson and IConfigurationObject or it could just as well be IDataObject and IRule (you may find that you know systems with such interfaces....)

What I'm trying to say is that having an interface per object type is a design decision, not a testable design decision. You can create testable designs that use interfaces designed well, with objects that implement many interfaces, or you could have a testable design that has one interface that all objects implement and nothing else. Yes, you can have a testable system with a less than perfect design, and you can have a well designed system with no testability. The beauty is combining the best of both worlds. Eli's fear is that testability will come instead of good design decisions -I say that it does not have to.

Consider an object that takes an IDataObject interface into one of it's methods:

bool IsValidForSave(IDataObject data);

 in the production code, what gets sent to that object could be some class that looks like this:

public class MyPersonData:IDataObject, IConfigurationHolder, ISerializable

...

Is MyPersonData exactly the same is IDataObject? hardly. Is the IsValidForSave method testable? much more that it would have been if it took exactly a MyPersonData object.  In fact, in many systems, MypersonData would look like this:

public abstract class DataObjectBase:IDataObject, IConfigurationHolder, ISerializable

public class MyPersonData:DataObjectBase

...

Still, this does not change a thing for testability. It's just a design issue. Testability and OO design can go hand in hand, it's just a matter of figuring out where they can go in parallel, and where only one of them can make a decision.

I think Eli has come across one of the issues that easily belongs into the first camp.

Eli's Analogy of "Testability" when referring to a TV Set

In the comments to Eli's post, In response to a question, Eli tries to make an analogy to a TV set(bold by me):

"Making your code testable, is like creating a modular TV Set, with an opening at the back of the set that you can easily open (without screws) put your hand through, dislodge the Remote Receiver (which you can switch with a mock Remote and test the TV), same goes with the volume controls, speakers etc. etc.
This is great and now you can sell the Modular TV Set to the world.
The thing is that it is more expensive to make (in code this means that the code is more complex) and there is no real business value. Who wants to pay more for a modular TV set?
Of course you can say that the TV set have been tested and this has value. But then with TypeMock you can do the same and lower the price of the TV set because you don’t have to make it modular. "

A TV set is not modular?

  1. Not modular: TV Sets (like PC internals) are modular. They are so modular that many parts in the TV belong to different manufacturers. Yes, there is a limit to the modularity, but the very fact that you can repair a TV by changing one of it's parts for a new one without replacing the whole TV makes a big case exactly for modularity.
  2. More expensive to make: I'm not so sure. Because it is modular, the TV is made of pluggable parts, some are very cheap to buy and put in, thus making the process much less costly (do you know of a TV maker who makes all the parts of their TV in house? much like PCs - that's not a cost effective option anymore - that's why "made in Taiwan" parts can be found in practically every piece of electronics on the earth).
  3. No real business value: Hardly true. It's relatively cheap to repair, build and make newer models based on the same structure with better parts (i.e high-end models) with very little investment in more design if at all.
  4. Pay more for a modular TV set: Again - consumers today pay much more for electronic items which are specifically design to work only with their own parts(luxury items) - That's like buying a high end Sony TV in which replacing or repair costs 3 times as much as any other TV on the market simply because it's not "generic"
  5. With TypeMock you don't have to make it modular: I'm not sure how this fits the analogy. perhaps Eli was referring to the way the TV has been tested for quality. Still, even if it ha cost less to test the TV somehow, the consumer would be getting a beast that has no replaceable parts - just one big black box that, in the event of a problem, takes longer and is costlier to fix, it would seem.

 

The issues I'd like to refer to in the next posts are:

  • Perhaps I was talking about making your design a "Test-Enabler", rather than "Testable" - allowing you to test your own code that uses that Test-Enabler system (FxCop is a good example of the opposite of a test enabler)
  • If you are creating a framework: you want to avoid the framework from being a "Test-inhibitor", so that people using it can test the code that is based on your framework without problems
  • SDKs are hard to test even if you had TypeMock, because you would need to know exactly which internal implementation to override. You'll have to dig in undocumented territory to do this. FxCop is a good example: how would I test my FxCop rules using TypeMock? How readable would those tests be?
  • there are two sides to testability:
    • Internal testability: How easy is it to test your own software,
    • external testability: how easy is it for others writing code against your APIs to test their own code.
      • (Client vs. server testability)
    • Every server is also a "client" in a way of testability. even the simplest class is a client of the .NET Framework.