TDD and the Single Responsibility Principle
Amongst the 5 SOLID principles, I found that the one which is the least tangible is the “Single Responsibility Principle” (SRP). Even though this is the most well-known and intuitively easy to understand, the questions “What exactly is ‘Responsibility’?” or “How do you determine whether a class has exactly one Responsibility?” is not trivial whatsoever. Obviously there are many cases that the violation of this principle is evident, but there are many more which fall in the grey area.
It’s often being claimed that TDD helps guide you to develop classes which adhere to the SRP classes (I admit that I also used this claim quite often). While this is basically true, this is also one of the main difficulties and causes for debates about TDD. Essentially, when you’re doing TDD, you’re constantly asking yourself: “What’s the core responsibility of the class that I’m currently developing?” and consequently you should Mock (/Stub/Fake/<you’re favorite term here>) everything else. However, when I’m doing TDD I find myself constantly struggle with the answer to this question and keep finding responsibilities that I can extract out. In most cases, I’m trying to be pragmatic and eventually I’m making some compromises. Because if not, I end with many more classes and many more tests than I would otherwise, and it looks like an overkill, even for a purist like me.
How small is a ‘unit’?
A different question that is often raised when talking about TDD, is “How fine-grained my unit-tests should be?” (or: “What is a ‘unit’?). This is pretty much the same question but from a different perspective. This question has a very important impact on the maintainability of the code-base, but then again, it doesn’t have a clear answer: From one hand, if your tests (and ‘units’) are very small and fine-grained, they are more composable/replacable etc. But on the other hand, having too many small ‘units’ hinder refactoring.
Recently I had a pretty small piece of code that I had to write. I decided to use this task as an exercise to see how far I can get with TDD and take the “Single Responsibility Principle” to its completeness. I tried to extract every little responsibility which is not the core of the class I was working on to a dependent class. I went through the process of TDD throughout these dependent classes as well, making them Single Responsibility as much as I could. Indeed, I ended up with quite a lot of classes, interfaces and tests, in comparison to the low complexity of the task. Even then, I felt that I could do “better”, and extract some more responsibilities out, but it was starting to get really absurd.
SOLID and Functional Programming
However, when I looked at the code I created, I noticed that virtually all classes had one simple method (implementing an interface), plus a constructor which only takes the references to the dependencies. This pattern reminded me the concept of a closure (a.k.a lambda expressions). A closure is like a method pointer, but it also contains some contextual data that it can reference.
I haven’t had the time to try it out, but I’m sure I could refactor the code I created in the following way:
- Replace all interfaces (that have 1 method) with delegates that correspond to that method
- Replace each constructor with a public static method that returns the delegate corresponding to the interface that the class previously implemented
The concept of “closures” is of course at the heart of another well-known concept, which is Functional Programming.
Declarative programming and TDD
Functional Programming also implies Declarative Programming, and Declarative Programming is often described as a language (or a style) that allows you to define what the program should do rather than how it should do it. But that’s also an attribute of a good test: it should describe what the program should do, rather than how. This may look like that in this case testing is redundant (one of the core ideas in TDD is DRY, or “Remove Duplication”).
Does it mean that programs written in a Functional language (e.g. F#) should not have tests (as long as it uses only “pure” functional constructs)? I find this question to be a tough one with no clear answer.
One difference between defining “what a program should do” in declarative programming, to the “define it” using tests, is that in the tests you can only define specific examples and not a general rule. (1) For example, suppose you want to write a method that accepts a string parameter and returns the same string appended with a period (‘.’). In order to test it I can define the following tests:
|“ “||“ .”|
Note that the definition itself (1) is complete, and technically speaking, you could implement the method without the examples given by the tests (2). However, as humans, most of us find examples easier to understand than pure definitions. Of course, the drawback of examples is that they’re (almost) never complete. In the above example, you can’t be sure that the method behaves correctly for any string other than those 5 that appear in the test cases.
This is a very simple example, but if you consider real-world definitions of full applications or even pretty simple methods, then even if you get a correct and full definition without examples, you’d probably have hard time to understand it correctly or prove that it behaves correctly at all cases.
Given a complete and correct definitions (which in reality is very rare, and even unreal for anything other than trivial) one can prove the correctness of the code without any test. In theory, the definitions themselves could be written in a declarative programming language the computer can run execute them directly, without the need of a “programmer” to translate the definitions from English to a programming language.
However, in reality, when complexity grows, we’re not very good at writing or even understanding complete and correct definitions, and therefore we need examples. These examples can be used as tests that we can easily understand to prove that (at least in the tested scenarios) the program is correct.
Because the tests are never a complete definition, the process of writing them is even more important than the tests themselves. This is because the process of seeking for meaningful, yet simple examples helps refine the definitions.
Back to the original topic of TDD and SOLID – I guess it could be said that “pure” SOLID code has a lot in common with (if not to say “is…”) functional style programming. And regarding the granularity of the tested units – I guess that there’s no one correct answer here. It’s like deciding where examples are necessary to explain something and when they’re redundant.