Test Driven Development is a tried and proven method of software development that helps you validate your work as you code. Previously I’ve talked about the benefits of TDD, and why it is successful. I could continue with an Intro to TDD, but I’ve found other sources that do a great job already of explaining the process. Instead, today I’d like to focus on best practices which lend themselves well to TDD, and if you aren’t following them already, they could make you a better programmer!
First: A Word to the Wise
Principles are not law. Some people become so caught up in their principles that they can’t bend them when a sane, sensible need for it arises. If you define “principle”, you’ll see “a fundamental truth or proposition that serves as the foundation for a system of belief or behavior or for a chain of reasoning.” Treat them as such and know that it’s okay to bend them when it makes sense.
The Principle of Least Astonishment
This principle applies to more than just software design, it can even apply to UI and UX. This principle has many interpretations but essentially it boils down to “don’t surprise people”. Components of a system should behave in a manner consistent with how users of that component expect it to behave – they should never be astonished. This can be applied to writing code too – if another dev works on something you wrote, it should work how they expect it to work. You wouldn’t expect a Login button on a web page to download a pdf, would you? That would be pretty astonishing. While an extreme example, you get the point, right? Similarly, you wouldn’t expect data persistence to live in crash-handling logic, would you? Nah, you’d probably expect it to be encapsulated in its own logic class. You’d probably also expect your method names to expressively convey their behavior, right? Don’t surprise people: make sure your design makes sense. This can help you in TDD, as avoiding astonishing choices will help your tests stay simple and clear.
When You Need It
This is more commonly called YAGNI, or “You aren’t gonna need it”, but I coined “When you need it” and always preferred to call it that. It sounds more pleasant, I think. This principle states “a programmer should not add functionality until deemed necessary.” Ron Jeffries said it well: "Always implement things when you actually need them, never when you just foresee that you need them." If you consider yourself an engineer like me, you probably get excited coding, thinking of neat ways to solve problems and help you out in the future. However, it’s easy to fall into bad habits and over-engineer. A good rule of thumb is “well, do I need it now?”. If you find yourself saying no, then hold onto the thought, share it with your co-workers, make notes of it to remind yourself, but don’t cross that bridge until you get there. This helps you streamline development and keeps you on track for your task. In TDD, it can help prevent you from spending time writing tests for features that you will never need.
The SOLID Principles
If you aren’t already familiar with the SOLID principles, you should be! They are core to my development, and I tend to think about them constantly: both during early development and refactor. They are powerful principles for Object-Oriented-Programming; encouraging stable yet adaptive design, code reuse, and small & simple classes.
S: SINGLE RESPONSIBILITY PRINCIPLE
“A class should only have a single responsibility (i.e. only one potential change in the software’s specification should be able to affect the specification of the class).”
This is probably my favorite principle in SOLID, although it can be confusing to grasp at first. Where do you draw the line at “this is a responsibility”? I think It changes on a sliding scale depending on the scenario at hand; sometimes you need more granularity, sometimes less. A good example is writing a program to print a report: you have the report data already, but you need a format to display it (formatting) and a means to print it (printing). Rather than write a class which does both, it would be better to write 2 classes which work together. If the Formatting spec changes in the future, only that class needs to change, and the printing class will work fine independently; or vice-versa.
How does this help TDD you might ask? Well, it keeps your classes simple and small, which makes your tests simple and small, making your units of work saner and more clear. The practice will also help you learn how to “chew an elephant” (or, break a huge job into smaller, more manageable pieces).
O: OPEN/CLOSED PRINCIPLE
“Software entities should be open for extension, but closed for modification.”
When you design your classes, you shouldn’t allow their logic to be modified, but only extended. This is an especially good idea when designing an API, especially one that will be used by other development teams. You want to ensure that your API works as you intended it to. It can be extended to fit new needs, but if it ever needs to violate the core logic it provides, then a new class should be created.
I’m going to be honest with you though, I often break this rule. Some situations I feel lend themselves well to abstracts and virtual overrides – like a helper class for a startup config on a WebAPI. The details of the config can change per-project, but the implementation of the config is always the same. Still, I try to follow the O/C principle as much as I can, unless it makes sense to break it. How does this help TDD? Well, it helps guarantee that usages of new subclasses shouldn’t break your tests.
L: LISKOV SUBSTITUTION PRINCIPLE
“Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
The basic premise is that if S is a subtype of T, then objects of type T may be replaced with any object of type S. But what does that mean? Take a real world example: I recently designed a repository pattern API for Springthrough, which allows us to contract our CRUD behaviors into one interface (IRepository<T>) for Inversion of Control. We have EntityFramework, DocDB and EasyDocDB subclasses, but we needed a way for them to generically handle some kind of basic entity type in order for the CRUD operations to work the same across all potential DB platforms. To accomplish this, I created an IDataModel interface, which just defines the bare minimum for what our Repo needs of its entities: just a key. Now when we make new data models, we inherit from this interface. Therefore, all subclasses of IDataModel can be used in the IRepository<T> generically, and we accomplish the principle of substitution because any IDataModel subclass can be used with our IRepository<T> without altering the correctness of the program.
The same mentality can be extended to many other situations. Why does this help TDD? Well, similar to Open/Closed, it gives you confidence that new subclasses shouldn’t break your tests. It also helps you design generic solutions to solve a generalized problem for many different types, which promotes code reuse, reduces maintenance, and makes for a simpler code base.
I: INTERFACE SEGREGATION PRINCIPLE
“Many client-specific interfaces are better than one general-purpose interface.”
You shouldn’t design your interfaces so that you end up with subclasses that contain contracted methods that the subclasses don’t use. The intent is to keep the classes decoupled, and with the help of the Single Responsibility Principle, it can help you design simpler and smaller classes, which are easier to refactor and redeploy. It helps promote better code coverage within TDD.
D: THE DEPENDENCY INVERSION PRINCIPLE (AKA INVERSION OF CONTROL)
“One should depend upon abstractions, not concretions.”
If you’re following the Inversion of Control practice with Dependency Injection, you’re probably already doing this right. The idea is that you should write your code to function off the most abstracted (bottom-most) object possible, usually an interface. This allows you to easily create new concrete subclasses of a type and simply replace old usages with the new subtype in your program.
Following all of the other principles, you should be able to do this without any surrounding code change, and the system should still be correct. If coupled with Dependency Injection, this practice can help you stay agile. Oh, that third party nuget package you used to show Pie Charts is deprecated and no longer supported? No problem! Spin up a new concrete of your IDisplayPieCharts interface using a different third-party package and just update the registration in your AppContainer. Boom! All usages of your IDisplayPieCharts interface have been updated to use your new subclass! How does this help TDD? Well, it allows you to change implementation easily without having to rewrite the surrounding code or tests because you depend on an abstraction and not a concretion.
I hope you enjoyed the read, and maybe you picked up a thing or two. I know these practices have always steered me right, and they certainly helped me be not just a better programmer, but a better test writer!