Hamburg Style TDD
There are a number of „TDD styles“ (or even „schools of TDD“):
And I cannot identify myself with any of them. Not 100% at least.
Recently I attended a talk by Marco Emrich who showed the above styles next to each other - and I kept shaking my head. There was some usefulness to all of them, but none really convinced me.
Then I saw a tweet by David Völkel in which he mentioned doing the Diamond kata „Munich style“. That triggered me to do it, too - and also Marco’s example from his talk, the Bank kata.
Styleguide
But what am I doing differently? Am I doing some kind of TDD at all? What’s TDD in the first place?
My understanding of TDD
TDD is mainly characterized by a simple process with three steps:
- Red
- Green
- Refactor
That means it’s a test-first approach to programming: first write a not passing test (red), then write production code to make the test pass (green).
What takes it beyond test-first, though, is the refactoring step. By that TDD acknowledges that production code should not only pass tests, i.e. show some runtime behavior, but also be of a certain form (which is not relevant to its behavior).
Also TDD clearly states its process should be run through multiple times. TDD explicitly is an iterative approach to programming.
And then TDD urges you to let test cases increase in their difficulty from iteration to iteration. With each iteration a bit more value is added to the production code. TDD thus also an explicitly incremental approach to programming.
There is little said, however, about how each step should be performed. But getting a test to green should only involve the simplest possible addition to the production code (application of KISS principle). And refactoring should eliminate duplication.
Since Kent Beck is the „inventor“ of TDD it’s no small wonder that also his „for elements of simple design“ are of relevance:
- Passes the tests
- Reveals intention
- No duplication
- Fewest elements
That’s pretty much it, I guess. That’s TDD. Easily stated - but difficult to implement. The problems start, I think, when applying all this blindly. Rules seem to ask for obedience. And so developers are trying to obey - with very mixed results and quite some frustration.
No small wonder, different „schools of TDD“ emerged.
The matter is and the problems are too complex for a single set of rules and a single approach. On top of that there is probably an unknown amount of loss in communicating TDD: Kent Beck surely is the master of TDD - but does he (or even can he) convey everything he does/think/know in a book or several blog posts? I don’t think so.
When communicating one’s own „being“ much cannot be said explicitly. Much is falling through the cracks when preparing a descriptive document. Being can only fully be experienced by watching someone in a not prepared situation.
What happens when a lossy communication is taken for something lossless? Confusion ensues. Applying the lossy content will not lead to the same results as for the originator. This leads to frustration, conflicts, lots of discussion, or worse.
This has happened for Agility in general and also for TDD as part of Agility, I think.
Only experience counts
From the inevitability of lossy communication (and before that reflection about one’s actions) follows that adherence to rules cannot be the goal.
Rules are nothing! The only thing that counts is success. If you consistently experience success without following certain rules, whatever you do is superior to the rules.
Rules thus are only suggestions for how to achieve results. Nothing more, nothing less. They are invitations - and can be declined.
Follow rules at your own peril.
Don’t follow rules at your own peril.
To improve your results, your task is not to follow rules, but to evaluate your actions - and then adjust your approach if you find the results lacking some quality. You cannot offload responsibility for results to certain rules. The responsibility always is with you.
Kent Beck, Robert C. Martin, J.B. Rainsberger, David Völkel, or Ralf Westphal are only reporting (personal) experiences and their humble reflections on why they think there might be causal connections between doing something and getting certain results.
If you feel inspired by that and try to reproduce their results, stay alert. Lots of things can go wrong. And not all that was going on when they made their experiences has been/could be reported.
Trying harder to follow whatever account you read to overcome some information loss thus is natural. Also it’s natural to adapt whatever you’ve heard to your situation. You’re entitled to your own experiences and reflections. Maybe they just deviate from what you’ve read, maybe they are in outright opposition.
Any result, any approach is ok. Just remember: reflect on it.
My reflections
That’s what I’ve done: I reflected on why I wasn’t satisfied with the results I got from applying TDD.
To make a long story short, what I found was: TDD was playing dumb.
TDD is a problem solving tool. The problem being: conversion of requirements into code. And to be more precise: into logic plus structure. Logic to create the desired behavior at runtime, structure to keep programmer productivity high over an indefinite period of time. TDD is about functional and clean code.
That’s great! I’m all for this result.
But I beg to differ with regard to the approach.
Where I agree:
- I agree with the basic test-first approach of TDD.
- I agree with the importance of refactoring.
- I agree with revealing intentions.
- I agree with avoiding duplication.
- I agree with avoiding overengineering.
- I agree with the importance of passing all tests.
But there are a couple of points I don’t agree with:
- Refactoring is great. But even better is to not need to refactor. If I can come up with (pretty) clean code right from the start, why should I refactor.
- What to refactor to? TDD is barely making any statement about that, which is one of the reasons why many supposedly TDD based solutions I’ve seen are… not clean.
- Overengineering sure is an evil to avoid. But something that might look like overengineering to the code author today might be a helpful structure to a code reader tomorrow. Where to draw the line? My feeling is, TDD users are carried away too often by the mentions of KISS and „fewest elements“. „Elegance“ is still in high esteem, even though it’s a twin of obscurity.
And finally: What’s bugging me again and again when looking at TDD examples is the neglect of easily achievable insight. TDD proponents are dumbing themselves (and thus the process) down in a drive to… hm… to what? Maybe to avoid overengineering? Or to go faster while writing code?
Yes, I guess it’s the will to be faster and to avoid BDUF. BDUF is costing time now and time later when it needs to get adapted to new information. BDUF is bad. We know that, right?
Sure, BDUF is bad. By definition. It’s evil because of the B. Big design is deferring feedback.
DUF, though, is not bad in my experience. Designing a solution before coding is even inevitable. At least I need to think before coding. I need to make a plan before I create structures and arrange logic. Sometimes the thinking takes 10 seconds, sometimes 10 minutes or 30 minutes.
TBC (Thinking Before Coding) to me is essential. It’s a discipline of its own. It needs to be trained, it needs to be taught.
But TBC has vanished from curricula and is not visible in popular TDD examples.
I’m not saying TDD proponents are not thinking before coding. What I’m trying to get across is that such thinking is not made explicit. It has no form. It’s not expressed in code or in any other way.
The incremental tests of TDD are no expression of such TBC in my view. Because test cases are only about understanding requirements, not designing a solution. Tests are no models.
Models, however, are badly needed, if coding is supposed to progress smoothly.
The perennial cycle of software development
I want to add another iterative process to TDD’s red-green-refactor (RGR). It’s simple, it’s natural, it’s even inevitable:
- Analyze
- Design
- Code
The ADC iterations are sometimes congruent with the RGR iterations, but sometimes not. RGR can happen inside of C.
In any case I find it important to emphasize these three step process because I think it’s what software development is about, and not only software development if you replace „Code“ with some other implementation discipline like „Paint“ or „Cook“.
ADC is to be cycled through many times on different levels of detail. It’s to be used iteratively and even incrementally if possible.
And, yes, the D sits right before the C. So it’s design „upfront“, it’s thinking before coding. Like it’s listening and asking before designing.
In my „school of TDD“ „students“ are asked to first analyze requirements. That means they need to build a solid understanding of the problem. And they need to express this understanding in an unambiguous form open to scrutiny.
Only once understanding is clear they move on to create a model for a solution to the problem. A model is the result of the design step. But of course a model is not „the real thing“, it’s not the solution yet. A model is a more or less abstract description of an approach to solve a problem. It’s declarative, whereas the solution, the logic is imperative.
And finally they implement their model using the means of a programming language. Implementation is guided by the model and thus produces less waste. To me implementation mostly is boring and a comparatively mechanical task. Designing the model on the other hand is exciting and highly creative.
My feeling is, design (and also analysis) are underrated steps in software development. And coding is an overrated step.
Making the ADC loop explicit is thus what I find important. It’s more important than doing TDD the Chicago style or London style. Hence I’d call my approach to TDD eclectic. It’s drawing from all styles if that’s what’s needed to further the cause of creating correct code plus clean code.
However, there’s one main distinction which leads to a different stance regarding mocks and other substitutes: I believe that functional dependencies are to be avoided as much as possible. They make code hard to read and hard to test. What my design and then coding are striving for is adherence to the IOSP (Integration Operation Segregation Principle) which I derived a couple of years ago from Alan Kay’s definition of object-orientation.
But enough with all the theory. What does „Hamburg style TDD“ look like when applied? Check out the following articles for demonstrations: