Test Driven Development
It seems to me that most or many treatises on best practices for software engineering in this day and age advocate for Test Driven Development (TDD). I have read about this methodology, both on blogs and in a book that I ordered from Amazon (Test Driven Development By Example). I have put it into practice in varying situations, and I enjoy the clarity that it brings to the development process once you manage to wrap your head around the idea of writing non-compiling, non-functioning tests prior to writing actual code.
That said, it isn’t a process I prefer to follow to the letter. I have tried, and it doesn’t suit the way I do my best development. I have read accounts of people saying that it simply takes some getting used to and that people object to it because they want to “just code” and are less productive downstream in the development process because of this, but I don’t think that’s the case for me. I don’t delude myself into thinking that I’m more efficient in the long run by jumping in and not thinking things through. In fact, quite the opposite is true. I prototype with throw-away code before I start actual development (architecture and broad design notwithstanding–I don’t architect applications by prototyping, I’m referring to adding or reworking a module or feature in an existing, architected application or creating a new small application)
Prototyping and TDD
As far as I can tell, the actual process of TDD is agnostic as to whether or not it is being used in the context of prototyping. One can develop a prototype, a one-off, a feature, or a full-blown production application using TDD. My point in this post is that I don’t find TDD to be helpful or desirable during an exploratory prototyping phase. I am not entirely clear as to whether this precludes me from being a TDD adherent or not, but I’m not concerned about that. I’ve developed and refined a process that seems to work for me and tends to result in a high degree of code coverage with the manual tests that I write.
The problem with testing first while doing exploratory prototyping is that the design tends to shift drastically and unpredictably as it goes along. At the start of the phase, I may not know all or even most of the requirements, and I also don’t know exactly how I want to accomplish my task. What I generally do in this situation is to rig up the simplest possible thing that meets one or a few of the requirements, get feedback on those, and progress from there. As I do this, additional requirements tend to be unearthed, shifting and shaping the direction of the design.
During this prototyping phase, manual and automated refactoring techniques are the most frequently used tools in my toolbox. I routinely move methods to a new class, extract an interface from an existing class, delete a class, etc. I’ve gotten quite good at keeping version control up to date with wildly shifting folder and file structures in the project. Now, if I were to test first in this mode of development, I would spend a lot of time fixing broken tests. I don’t mean that in the sense of the standard anti-unit-testing “it takes too long” complaint, but in the sense that a disproportionate number of tests I would write early on would be discarded. And while the point of prototyping is to write throw-away code, I don’t see any reason to create more code to throw away than necessary to help you see the right design.
TDD and Prototyping in Harmony
So, here, in a nutshell, is the process that I’ve become comfortable with for new, moderately sized development efforts:
- Take whatever requirements I have and start banging out prototype code.
- Do the simplest possible thing to satisfy the requirement(s) I’ve prioritized as highest (based on a combination of difficulty, prerequisites and stakeholder preference).
- Eliminate duplication, refine, refactor.
- Satisfy the next requirement(s) using the same criteria, generalizing specific code, and continuing to refine the design.
- Repeat until a rough design is in place
- Resist the urge to go any further with this and start thinking of it as production code. (I usually accomplish this by doing the preceding steps completely outside of the group’s version control.)
At this point, I have a semi-functional prototype that can be used for a demo and for conveying the general idea. Knowing when to shift from prototyping to actual development is sort of an intuitive rather than mechanical process, but usually for me, it’s the point at which you could theoretically throw the thing into production and at least argue with eventual users that it does what it’s supposed to. At this point, there is no guarantee that it will be elegant or maintainable, but it more or less works.
From there, I start my actual development in version control. This is when I start to test first. I don’t open up the target solution and dump my prototype files in wholesale or even piecemeal. By this point, I know my design well, and I know its shortcomings and how I’d like to fix and address those. I also generally realized that I’ve given half of my classes names that no longer make sense and that I don’t like the namespace arrangements I’ve set up. It’s like looking at your production code and having a refactoring wish-list, except that you can (and in fact have to) actually do it.
So from here, I follow this basic procedure:
- Pick out a class from the prototype (generally starting with the most abstract and moving to the one that depends most on the other ones).
- For each public member (method or property), identify how it should behave with valid and invalid inputs and with the object in different states.
- Identify behaviors and defaults of the object on instantiation and destruction.
- Create a test class in version control and write empty test methods with names that reflect the behaviors identified in the previous steps.
- Create a new class in version control and stub in the public properties and methods from the previous steps.
- Write the tests, run them, and watch them fail
- Go through each test and do the simplest thing to make it pass.
- While making the test pass, eliminate duplication/redundancy and refine the design.
This is then repeated for all classes. As a nice ancillary benefit, doing this one class at a time can help you catch dependency cycles (i.e. if it’s really the case that class should A has a reference to class B and vice-versa, so be it, but you’ll be confronted with that and have to make an explicit decision to leave it that way).
Advantages to the Approach
I sort of view this as having my cake and eating it too. That is, I get to code “unencumbered” by testing concerns in the beginning, but later get the benefits of TDD since nothing is actually added to the official code base without a test to accompany it. Here is a quick list of advantages that I see to this process:
- I get to start coding right away and bang out exploratory code.
- I’m not writing tests for requirements that are not yet well understood or perhaps not even correct.
- I’m not testing a class until I’m sure what I want its behavior to be.
- I separate designing my object interactions from reasoning about individual objects.
- I get to code already having learned from my initial design mistakes or inefficiencies.
- No code is put into the official codebase without a test to verify it.
- There is a relatively low risk of tests being obsolete or incorrect.
Naturally, there are some drawbacks. Here they are as I see them:
- Prototyping can wind up taking more time than the alternatives.
- It’s not always easy to know when to transition from prototype to actual work.
- Without tests, regressions can occur during prototyping when a violent refactoring sweeps out some of the good with the bad.
- Without tests, some mistakes in your prototype design might make it into your official version recreation.
I think that all of these can be mitigated and I firmly believe that the advantages outweigh the disadvantages.
When not to do this
Of course, this method of development is not always appropriate. During a defect fixing cycle, I think tried and true TDD works the best. Something is wrong, so write a test for the right thing and modify the code until it happens — there’s no need to prototype. This process is also inappropriate if you have all of your requirements and a well-crafted design in place, or if you’re only making small changes. Generally, I do this when I’m tasked with new feature or project implementation that has a high level design already, and is going to take weeks or months.