The Production Value of Seams
There are some interesting points of views about testable design that I somehow missed catching in real time.
He then goes on to show what happens when you addTestability concerns to the some of the usual OOD principals.
He then shows how you can keep traditional OOP approach and still test your code using TypeMock.
I am firmly in Roy's camp when it comes for designing code to be testable. Before I goes on to explain further, I want to take issue with a couple of points in Eli's post:
Ctrl+Alt+B in Resharper will take me directly to the implementation. grep *.cs -S "interface\s+INameOfInterface" will do the same if I don't have Resharper. Not an issue.
No a code smell at all, in my opinion, just good seperation of the interface and its implementation. I may want to supply alternate implementation, which would be much harder if I had to a concrete class to work with.
Let me share a story. A while ago I was working on a system that needed to write to a file. The simplest code that I could write was:
That is not testable without using an invasive approach like TypeMock, so I choose this way:
I added two level of abstractions to the code, just to make it testable. Following Eli's approach, I had decreased the readability of the code and made it harder to understand and maintain. The problem is that I don't think that Eli considered the value of the seams that are created by writing testable code. (Take a look at the stats here, it was a bit more involved).
A seam is a core concept in Working Effectively With Legacy Code. A seam is a place where you can alter behavior in your program without editing in that place. They are crucial for being able to test your code. (Just to note, TypeMock create seams all over the place by simply being able to intercept any call under .Net).
Seams makes the code testable, but it also makes it a lot more flexible. Couple of weeks after I introduced the IFileWriterFactory, I had learned that the files that I was creating were to be consumed by a stupid batch process, which would copy them over to a mainframe system. If I happened to be in the middle of creating the file when the process start running, the mainframe would get a partial file, and chaos would enuse.
The standard way of handling this is to actually output two files, one with the real content, the second with an agreed upon extension, which basically tells the process "if you see a file with extension foo.csv.done, I have finished writing the file foo.csv". I had couple hundreds places in the application that I was writing files.
If I was following the traditional approach, I had to find all the places that I was writing to a file, and change the to:
As I mentioned, there were quite a few place that did it. Because I had a seam in place, I could simple change the returned implementation of the TextWriter that FileWriterFactory implemented, which would generate the ".done" file when the writer was disposed. You can bet that this was a valuable thing to be able to do.
In general, I find that testable design if far more flexible, because it means that you can inject different behaviors into the code. This means that you get more extensible code that you can later leverage to produce more business value. The simple abstraction that I have shown above is something that saved me two days of mindlessly going through the code and making the same stupid change all over the place.
So no, I do not believe that testability is not a first class concern, and I feel that it produce much better code in the end. Having more places where you can change the behavior without changing the code is a good thing, and it would end up saving you quite a bit of time. I can see it in Rhino Mocks all over the place, the fact that it is as decoupled as that means that I can take it in different directions (and offer extensability points for users) very easily.
The other side is apperant in many recent Microsoft releases, where internal and sealed are slapped on just about anything, and extensability points for something that they didn't explicitly planned are simply non-existant. This in turn greatly reduce the ability to work with the code.