TDD In Real Life – Part 1
TDD comes from the world of unit testing. It’s optimized for small pieces of code; small increments of functionality. BDD takes the test-first approach, adds functional and user semantics and tries to follow the same formula for the whole software.
TDD helps us build a class or a module. But in real life, we’ll need to build complete scenarios. BDD helps in defining those flows, but doesn’t help us design the software, like TDD does. Not to mention if we’re constrained with existing systems.
There’s an impedance mismatch. On the big picture side, we have stories (or even epics) that take a while to complete (especially if you don’t write them from scratch, and you already have components you need to maintain, upgrade and interact with).
On the other side of the scale, TDD allows you to design components , but it doesn’t provide the feedback about all the flow. Also, it doesn’t tell us if the emergent design is the “right” one. (PSA: There is not only “one” design, this is not Highlander.)
So what approach should we take? We want to use TDD for its benefits, but we have existing constraints we need to adhere to. If we’re writing a complete new feature, we don’t have any design at all.
Here’s my approach. It requires some planning and thinking ahead, which never hurt anyone. Much.
Let’s take it step-by-step
Step 1 – Identify the important stories
First, we need to identify the main stories (flows) we’re going to write. If we have them in a form of BDD tests, great. If not, we need to at least have a list of the stories/requirements that are important.
Why do this before designing a big solution that answers all the requirements? If we start before identifying the important stuff, we’ll come up with an over-engineered architecture that has a lot of unnecessary code (YAGNI) and what it always bring to the party: bugs.
Like with TDD, our intention is to write the minimal code that works. However, in integration or system tests, we need to do develop the minimal design that works.
That means we’ll try to build incrementally, completing a story before moving to the next, and NOT completing a component for all requirements.
One more thing: It is very important we state the requirements or stories as a flow in the system. That means operations that the user or the system does, where data is transferred and operations are completed. If that is not the case, we’ll find ourselves in this stage again after visiting step 2.
Step 2 – Define the acceptance criteria
This is extremely important, because if we just stick to “this flow should work” we’re leaving it open ended. And with that, we’ll be out of focus, building things we don’t need. Like with TDD, we specify how we expect to execute and check the behavior we’re interested in. And that requires that we know what the acceptance criteria is.
With BDD style tests, which are really examples it’s easy. Although even with those, sometimes we live the test in the Gherkin stage, and only later, when we have something to connect, we do that.
With plain stories and requirements, we run the risk of implementing something that is not needed. And, by specifying an acceptance criteria, we get the sense of the effort and complexity involved in the implementation. If we can’t say how we’re going to test the story, that’s a sign we need to do more thinking before jumping to development. Those might lead us to re-prioritize stories in step 4.
Step 3 – Identify the main components that the stories flow through
They can already be completed, half written, or non-existent. A story flows through the components, and it may either use them as-is, may need interface modification or extension, new coding or a full rewrite.
While this is a design step – we’re proposing a solution how we’re going to make the story work – it may not be the final one. We’re doing just enough design up front. This helps to confirm the feasibility of a solution.
When we map the stories to components, we can see options for design. We can see how the different pieces of the puzzle fit, and if we have problems we need to attend to.
There’ll be a separete post about design mapping, it requires a detailed description.
Step 4 – Re-prioritize the stories.
“Wait, now the developers prioritize?”
I know, it’s sounds a bit anti-scrum, but hear me out.
Even after someone (product owner or similar) had already prioritized the stories in terms of value, there’s still valid input that can come from the dev team.
Sometimes, there are dependencies between the stories. While story #1 is more valuable than #2, we won’t release without the first 10 stories completed. That means we can play with the order, if there’s a good explanation for it. In this case, if we can’t easily test or develop story #1 without having #2 in place first, we’d probably want to develop #2 first.
Note, we’re not questioning the value of the stories, rather how quickly we can complete them. If we deliver complete stories early, we get feedback early and can make decisions based on this feedback.
If however, everything we do is suspect to change, and learning is what leads all the stories, then the higher value stories delivery is more important, we go with those.
Agile note: If you want to think about it as the team providing information about risk and effort and then the PO re-prioritizes the list – fine. Eventually, in a good agile process, the whole team approach works best with backlog prioritization.
That’s enough for today. We didn’t even get to coding, we’re just thinking about design. We’ll get there next time, I promise.