There is a personal story behind this topic that I want to share. About five years ago I heard about this thing called Test Driven Development (TDD). For anyone unaware of this, it is where you write failing tests first and then write “the code” later to make them pass. It immediately struck me as interesting and the more I learned about it the more I liked it. It seemed like much more than a means of testing code but a design style that could transform the way we write software.
As I actually began to practice this discipline, it did completely change the way I write code and approach design. It has been very challenging and rewarding. I don’t at all consider myself polished or advanced but I have drunk the Kool-Aid and hey, who doesn’t like Kool-Aid!? I cover more specifics later but please, let me reminisce.
At the time when I was discovering this technique, I purposely sought out a team of developers who were TDD practitioners. As someone who is self taught and prefers to learn on my own, this was an area I knew I needed to learn from others to better get my head around the patterns. So for almost three years I was immersed with my new team. We often discussed the virtues of strong unit test coverage and TDD. When you roll with a group that all share a common set of values, it is easy to feel “right” and take some nuances for granted. Even if they are right.
Maybe the world IS flat
Six months ago I changed teams. The new team was different from my former team. One difference was a lack of unit testing. There were lots of tests but they were essentially integration tests and took a long time to run. So long that they could not be run as part of the build. Tests were always written after coding and one reason why was that the code itself was nearly impossible to test using typical Unit Testing techniques.
One of my initial thoughts going in was that this would be an excellent opportunity to make a difference for the better. I still believe this but I was very surprised at how some of my techniques and the patterns that I had come to embrace were questioned and given some rather skeptical critiques. Suddenly it felt that the things I had come to value in standards of design and build methodologies was a currency not honored in this foreign land.
Now it would have been one thing if I was working for some mediocre outfit of 9 to 5er developers. However many of these were people I consider to be very smart and passionate about writing good software. It was like having a bunch of Harvard grads insisting the earth is flat and watching yourself begin to question if the world really is round. Sure does look flat when you really look at it.
Not everyone likes chocolate and some who don’t are smart
Sidebar: I just learned today that Jonathan Wanagel, who runs Codeplex and is one of the smartest people I know in the whole wide world does not care much for chocolate. Interesting…
What is this Hippie code?
Here is a list of many of the things that turned some people off:
- Interfaces – adding too many types to the API, kills Visual Studio’s F12 and overkill when there is only a single production implementation.
- Unsealing classes, adding virtual methods or making some private/internal types public – Now we have to support a larger surface area and make sure customers do not get confused or shoot themselves in the foot.
- Eliminating static classes – now I need to instantiate a class to access a simple utility or helper method.
- Test methods with long method names resembling a sentence describing what is being tested –that’s just weird.
- A general concern expressed by many - We should not have to change code and especially an API just to add testability.
Some of these concerns are very valid. Honestly they are all valid. Even though I completely disagree with some of the opinions I have encountered, it must be remembered that introducing new ideas will be naturally distasteful to some if they do not understand the intent and it is therefore incumbent upon the bringer of the new ideas to clarify and articulate why such ideas might have value. I’m not claiming “mission accomplished” but this forced me to do a lot of “back to basics” research to understand how to communicate the value. While I feel the value deep in my bones, it has been very challenging to learn how to express the value to others.
Lets play Devil’s Advocate
Before I dive into the responses I have developed and am still developing for the above reactions to a different style of design I would like to spend some time defending the critics and skeptics. I work with very intelligent people and I respect the fact that they demand to understand why someone suggests a change in style and a radical change in some respects.
Personally I spent more than half my career not practicing TDD or writing proper unit tests. I too was very conservative about what my API exposed. I liked to write APIs with few classes. I liked “noun” style classes (employee, order, etc) with their own persistence logic and static Create methods. I thought this was elegant and a lot of others think so too. And honestly, I wrote some code that made some people a lot of money during this time.
Sealed classes, internal methods, oh boy!
Among the crowd I run with on the twitters, there is a lot of nay saying around sealed classes and internal methods. Don’t expect me to be changing my Add New Class template to create them sealed by default but I do think this deserves some thought. My background is largely in web technologies. This often means that my server code will never be exposed to anyone outside of my team. It is easy to adopt much looser rules around api exposure when you are the only consumer.
I am also involved in open source software. We are a small cadre of developers and we are not representative of the average developer. I know. That sounds elitist but it is true. I’m used to reading source code in lieu of documentation (not necessarily a good thing). I’m perfectly comfortable pulling down someone else’s code using any of a half dozen source control providers. And like many of my OSS peeps, I get extremely annoyed when I am trying to work with an API, find some code that looks to be just what I need and then see it is not accessible. I’d rather have a larger api that provides me heightened flexibility and extensibility over a smaller and easier to understand API – within reason of course.
However, when you work on software that physically ships to enterprise customers, you really do need to broaden your view. I might work for Microsoft, but if you call me an enterprise customer, I’ll cry. The fact is, it is important to understand your audience. Most developers don’t want to concern themselves with the innards of your code. That’s why they buy it. It should do everything they need it to do and be easy to figure out and difficult to misuse. No matter who your audience is, the public API is one of the most important things to get right. It should read like documentation and be self explanatory. Sometimes this means putting a curtain over large parts of your codebase. I’m still coming to terms with this, but I do believe it is a reality that deserves attention.
Testability for its own sake – That’s just fine
I used to feel an uneasiness when discussions would take this turn into warnings about the dangers of making code testable simply for its own sake. I would feel a sense of guilt about asking others to work harder just to make things easier to test. I’m over that. This is like arguing against quality for its own sake or simplicity just to be simple. If I have to tweak an API to make it testable, we have to remember that we are not only doing ourselves a favor but everyone who will be using our software can now test around it as well.
While TDD and similar practices have been fairly mainstream in other communities for a while, it is becoming more so in the Microsoft dominated technologies (where I work) and much more so than it was just a few years ago. We need to understand that testability ships as a feature to our customers. Software that is difficult to test is not just perceived as a nuisance but its overall quality can be called into question by virtue of a lack of testability.
So what is so great about testability?
Perhaps I’ve gone far too long in this post before championing the virtues of testability not to mention some clear examples of what it is. I can hear others questioning: what are you talking about? We have QA staff and huge suites of test automation. Testability? Lets not get carried away.
Yes. Testability is very much about testing the code that you have written. Specifically, I am referring to the ability to test small single units of code (an IF block for example) without the side effects of surrounding code. I might have a method that queries a database for a value, sets the state of the application depending on the value queried and logs the result. I want to be able to write tests that can check that I set the application state appropriately but not that I got the right data from the database or that I successfully logged the activity. I’ll write other tests for that.
Having a code base well covered with these kinds of tests can create (but does not guarantee) a very high quality bar and allow developers to spend more time writing features and less time finding and fixing bugs. The more logical paths your code can take, the more important this is. It is easy to innocently introduce a small change and inadvertently break several logic paths. This level of code coverage is your safety net from bug whack-a-mole. Each release without this coverage brings increased surface area for bug creation until the cyclomatic complexity drives you to a saturation point and now you spend most of your time addressing bugs.
Good test coverage and code that is easy to test invites exploration and experimentation. It is the fence that keeps us from touching the third rail. Low coverage incites fear into the hearts and minds of good developers. “Hmm. That sounds like a really interesting approach but we don’t dare touch that code because it is core to our business and we cant afford for it to break.“
Testable code is more about good design than a test automation arsenal
This is something that can sound odd to the unindoctrinated. At least it was not what originally attracted me to TDD but it IS what has kept me here.
In order to write code that is easy, let alone possible, to test with this kind of granularity, one must enforce a strict separation of concerns because you want to test no more than a single concern at a time. This may produce code that looks different to many teams and might look awkward at first glance. The code is more likely to have these traits:
- More types. More classes, more methods and more interfaces. As one ensures that each type contains a highly focused and intent driven purpose. Rather than having a person class that not only represents the person but also does a bunch of stuff to and with a person, one may now have several classes that look more like verbs than nouns to represent different interactions with a person.
- Smaller types and smaller methods. This goes hand in hand with the above. It does not take a genius (that’s why I figured it out) to discover that it is tough to test a method that does 20 things. And guess what? It is easier to read and understand too.
- More layers of abstraction and points of extensibility. This may coincide with the mention of more interfaces. As you tease out corners of code to test, you need to be able to apply protective tape over surrounding machinery that should remain untouched by the test. This may be because these surrounding areas talk to out of process systems that would bog down the performance of a test or engage in complex logic that manipulates values that must interact with the code under test. It is easier to “plug in” lighter weight machinery that acts on data very predictably, repeatably and quickly. The use of interfaces, dependency injection and mocks/stubs/fakes/etc come into play here and may make one not used to them feel out of water or like they are over engineering. One may react that this abstraction seems silly. Why create an INamingService when we only have one naming service. First, that is a fair point and should not be ignored. It is possible to over engineer and you need to decide what level of abstraction your scenario calls for. That said, once you gain competence coding in this manner, you often find ways to exploit these abstractions into rich composition models that would not have been possible given a more monolithic class structure.
- A larger surface area to the API. This is what many find the hardest to come to terms with and they should. There is A LOT to be said for a simple API and testability does not necessarily make this fate an inevitability. However it does make it more likely. With an application having more “building blocks” there may be a greater number of these blocks to interact with one another and exposing those interactions to the consumer may very well be a good thing. Also, just like you, your consumers writing code around your API may demand testability and the ability to abstract away all exposed blocks.
This design style facilitates change, improvement and happy developers
Code that is easy to compose in different ways is easier to change. This kind of a model allows you to touch smaller and more isolated pieces of your infrastructure, making change a less risky and dreaded endeavor. A team that is empowered to change and improve their code more rapidly makes for a happier team, happier business stake holders and happier customers.
Use modern tools for modern programming techniques
Ok. I’m gonna call TDD (because that’s usually what this methodology becomes more or less) a “modern programming technique.” There are tools out there that are designed to make these practices easier to implement. As a primarily C# developer, these tools include Resharper, an IOC for dependency injection, a good mocking framework and a modern test runner like XUnit.
Resharper makes a lot of the refactorings like extracting interfaces and finding interface implementations and their usages easier to discover. It provides navigational aides making work in a code base with more types much easier.
An IOC facilitates the creation of these types and makes it easier to manage either swapping out one type for another or discovering all implementations of a given type. One may find constructors with several types being injected which would be extremely awkward to “new up.” With a properly wired IOC, it handles the newing for you. Almost all IOCs (don’t create your own) provide solid lifetime management as well which provide the utility of singleton classes but without their untestability.
The wrong question
So lets return to the core question of this post. Is changing an API or design solely for testability a good practice? I would argue that we have not properly phrased the question. Begin by exploring your definition of “testability” and you may well discover that the “sake of testability” is not nearly all that you are after.