It is a well-established concept that the best way to get 100% code coverage is to write your unit-tests from the very beginning of a project, such that each test influences the code, and the code immediately influences the tests as it evolves. This is more than just a strictly dogmatic approach to code construction reserved for ultra-ninjas and people who hire dominatrices (I’ve never had to pluralize that word before, I swear) to dig their heels into their backs for pleasure, this is good advice. Test-driven development is the best way to get solid code coverage, but for reasons often beyond our control we don’t have the luxury of TDD. Either because we’ve inherited a brownfield project with lots of legacy code, or because our deadlines are tight and we just don’t have time to convince our pointy-haired bosses that having tests provide business value. Or we’re just lazy.
This puts us in a bit of a bind. As good developers we know that having good code coverage is a good thing. It’s like flossing, or eating our vegetables; we know we should do it, we know it’s good for us and will prevent a whole host of misfortunes later on, but we don’t because we think it’s a chore. We might write a few tests later, when we have time, or when we need to debug something, but by then the code has morphed into untestable spaghetti. All hope of an isolated repeatable unit-test has gone out the window, and the best we can hope for is something that requires a lot of setup, and database, and possibly some services, and a bit of luck to work once, followed by more code to tear-down all the changes the test has made. Sound familiar?
So, you’re not into TDD, but you don’t want writing tests to feel like agony. Luckily, there is a middle ground. Write your code as if you plan to test it later. Design your code in such a way that testing it should be easy. If you follow a few fundamental rules you will find that in addition to being protected from regression bugs when you write your tests, your code will get measurably better and contain fewer bugs simply by virtue of the WAY you are constructing it.
1) Limit side-effects.
Okay, if you’ve ever listened to any academics talk about functional programming you are probably sick to death of hearing about immutability and side-effect free code, and I’m not advocating exactly that, but simply asking you to think about how your code will be used with respect to state. Does your code behave the same way the first time as it does the third? This concept touches on a function’s purity, in the mathematical sense. Put simply, if a function is pure then testing it is easy. If a function uses lots of state and is highly coupled with other classes, then testing becomes that dreaded chore we are hoping to avoid. Set yourself up for success and keep your classes and methods small, uniquely responsible for its’ functionality, and have fewer dependencies (both in terms of state and additional classes it needs to accomplish its’ job), and you may find that your code is not only more testable, but also easier to understand and more resilient to change. Isn’t that nice?
2) Depend on abstractions.
Your eyes just rolled back thinking about the D in S.O.L.I.D. right? Yeah, well, I’m not sorry. It’s good advice if you apply it correctly. I’ll use the canonical example of LINQ which I assume everyone in the .Net world has used by now (and should probably stop abusing). The power and flexibility of those query methods is directly related to a single elegant abstraction ( IEnumerable<T> ). Each method in that stack only needs to know how to enumerate some collection to be effective. These methods work on linked-lists, arrays, sets, even some crazy infinite “virtual collections” that materialize data only when the next item is requested.
In the same way, your own code can be flexible with respect to the arguments used to call it. Generalize! Does your method really require a concrete instance of Collections.Generic.List<T>? Are you adding and removing elements? If so, maybe try IList. Need just the Count property? Downgrade your required parameters to take an ICollection. It doesn’t even need that!? Your method just uses foreach over the collection of things and does something for each thing. Have I mentioned how useful IEnumerable is?
Your code is now much more useful, and also provides some added context about how it will use the parameter to the callers. IEnumerable gives them a nice guarantee that you (probably) won’t do something sneaky like adding or removing items from their collection. Obviously if you were malicious you could try casting it to some other interface that supported that behavior, and then tamper with the collection, but those are the sorts of ill-willed malcontents who would cast off their const-iness in C++ and we should just try to avoid them and hope for the best.
Stay tuned until next week when we talk more about writing testable code with some practical examples. Same smug time, same smug channel.