Published on

TDD: specify, satisfy, simplify

Authors
Kudos! 🙏🏻

I really want to thank Grzegorz Gałęzowski for patience, knowledge and sharing experience with me!

We have tons of discussions (or rather me asking a plethora of questions and learning from Grzegorz Gałęzowski), especially on Test-Driven Development and Object-Oriented Analysis & Design.

Grzegorz also wrote a book - Test-Driven Development: Extensive Tutorial - you might find it interesting and useful!

The ambiguity of TDD

"2025 and people are still posting stuff about TDD?" - some of Readers might think, seeing the title of this tale.

Developers can conjure applications without writing a single line of code, build with using English as "a programming language" (you know it isn't true, do you?).

In the age of AI-assisted coding, TDD seems like a relic of the past, because AI can generate code for us, right?

Some time ago I wrote a tale about TDD, trying to explain why "TDD" as a slogan is ambiguous and can lead to misunderstandings.

Writing tests after implementation is still a common practice, and many developers never grasped idea behind TDD, stating that "writing tests first" is just a waste of time.

As with other abstractions, the concept, "the symbol" - TDD - takes away details, trying to amplify the essence.

Test-Driven Development - "using tests to drive development" - what could possibly go wrong?

Many learn: "write failing test, make it pass, refactor", which describe how, not capturing what.

TDD is an engineering discipline, a workflow, that is meant to create conditions, the environment, in which one can deliver high-quality software - combining both functional requirements and quality attributes like modularity and maintainability.

That's why I deliberately decomposed TDD into: verification and specification.

But those two perspectives are also "artificial", mostly created to highlight those two important aspects of TDD.

Driven by specification?

Turns out that quite a lot of developers focus too much on tests themselves.

Tests are important, for sure, but they are materialization, an implementation, of something more abstract.

As well as "tests", "three steps" - "red, green refactor" or "write failing test, make it pass, refactor" - feel like not telling the story completely.

And it might be that's the case, as they are useful "mnemonics", to get them easily remembered.

But what do they actually mean?

As with (business) capabilities - "WHAT, not WHO and HOW" - it would be nice to capture that WHAT of TDD.

Here comes 3S, which stands for: specify, satisfy, simplify.

Specify

The first step is to specify the observable behavior, the expected results.

Such behavior specification must take the needs of the consumer, be it a human, a service, an object or a module, into account.

This is the moment when the miracle of "interface discovery" might actually happen - and one can observe Need-Driven Development in action.

Taking the consumers needs into account, treating it as an input (for both providing expected results and finding the "just enough" design), is one of the important ways for designign software, that greatly impacted and influenced me - outside-in approach.

But this requires one of the hardest things in software design: pretending to have a (wishful) amnesia and forgetting about the "designers" perspective (which strongly focuses on implementation details), putting oneself into the shoes of the consumer.

Such act of emphathy is hard, but crucial, and in a way it constitutes the essence of thinking like an engineer, a designer.

We can specify by writing tests, we can specify by writing bullet points or by drawing diagrams - no all of those hows will be automated and effective, but each will manifest what - the act of specifying.

The important part is to focus on what the consumer needs, not how it will be implemented.

Satisfy

The second step is to satisfy the needs, expressed through structured specification, with the easiest implementation of knowledge representation.

And by saying "knowledge representation", I refer to applying our current understanding of the problem, providing "logic" and "rules", with all required information to "make those decisions".

Additionally, "the easiest" has a very special meaning here - as you probably already know, dear Reader, Simple isn't easy.

In my current model of the world, "easy" means "the least effort required", in contrary to "simple", which actually might require quite some effort to achieve.

So this might take duplication, static and hardcoded values, and other "hacks" into account, to ensure that specification is satisfied.

All those hows manifest what - the act of satisfying.

Here is the moment when one can use "Fake It Till You Make It" or "Obvious implementation" approaches, to get the needs of the consumer satisfied as soon as possible (thanks Grzegorz for bringing that one up!).

This is also the time to put everything in the single file, completely forgetting how to organize things.

Why not putting things in the controller?

The act of simplification will come, soon.

Often times it might feel quite unnatural to even consider doing that, but one tremendously important part of quick transition between specification and needs satisfaction is providing the value (and as we know, the value is in the eye of the consumer), and followed by getting feedback.

And feedback is about learning.

Learning about the needs, about the problem itself.

So the sooner one understands whether the specified behavior is actually what the consumer needs, the better.

When feeling under control, take bigger steps.

When feeling uncertain, take smaller steps.

This is also the moment when focus on the current consumer needs is crucial - we don't want to produce something what is not needed, avoiding waste.

As we explored in another tale (Code, knowledge and "AI"), here are the words from Taiichi Ohno, the father of Toyota Production System:

Conclusion 🔍

Overproduction is the biggest waste.

We strive to satisfy the specified consumer needs, not more, not less.

Simplify

The third step is to simplify the implementation, trying to achieve quality attributes like modularity, understandability, maintainability, and so on.

If we used tests to specify consumer-expected behavior, we can see that those some of the needs were already satisfied by seeing "the green bar" (this might not mean we finished providing all required functions!).

If we used manual verification, we can hope we didn't break anything in between.

But remembering Rich Hickey's words about simplicity:

Conclusion 🔍

Simplicity often mean making more things, not fewer

Now we want to reflect on the implementation and make a decision - do we want to keep it as is, and continue with the workflow, going back to the first step, considering we don't have all the needs specified.

If we decided that it's the right time to improve the implementation, we can start simplifying it.

We can refactor the structure (of the knowledge representation) - introduce new objects, extract functions, put more boundaries, and so on.

It is also the time to take care of things like duplications and look for new abstractions (the importance of that process is nicely presented in this blog post).

Sometimes it will be renaming, introducing new concepts using classes, capturing important capability by using an interface.

All this effort is to organize elements and interactions between differently, so that we get something back (when it comes to "organizing things", you might be interested in The ambiguity of software architecture).

Such "simplifying" work can be moving things around - separate files, separate directories.

All those hows manifest what - the act of simplifying.

Specify -> Satisfy -> Simplify, repeat

The magic is in the workflow.

But actually, to be efficient and get the results, "the designer" (or "the operator") must know techniques, approaches, practices and mindfully spot "hacks" introduced while satisfying.

When a person not that experienced is left alone, "commanded" to "do TDD", quite often the result is suboptimal.

"Be soft on the operator, hard on the process" - as Deming used to say.

An engineering workflow

Also, just starting from "specification" step might be quite late - it might feel that when all people are busy doing their work in isolation, we reached the optimal way of utilizing brilliant minds of people.

Many times the perfect specification, with perfect and simple implementation, is horribly incorrect - because we missed the problem.

"We always can quickly do the wrong thing" or "we always can run fast in the wrong direction", as some say.

Collaborative, engineering workflow

It might be that the whole cycle starts earlier - when people meet and draw together, discuss together.

"When one teaches, two learn" - and it is not only about technical skills and knowledge, often times it's about things like the why of the currently explored problem, other possible ways of providing a solution.

The best would be to sit together and go through all three steps together - specifying, satisfying, simplifying.

The amount of learning might be huge, and the quality of the result might be outstanding.

There are prerequisites, of course - psychological safety, technical excellence of at least some people in the software teaming session.

TDD = Consumer-expected behavior specification through tests?

So, dear Reader, next time you hear "TDD", think about 3S - specify, satisfy, simplify.

TDD itself can be thought of as a symbol, an abstraction, but I believe it might be really worth exploring all the steps and find what we want to achieve in each of them.

Such deepened understanding might contribute to actually using the discipline in much more mindful, more aware way.

It is important to remember that all those steps are about what we want to achieve, not how we want to do it.

So specification might not include automated tests, but then we are risking losing the feedback loop, later defects and much worse maintainability.

The best person to trick and deceive us is ourselves, so we can easily say that "writing tests slows us down", but it quickly might turn out to be a false economy.

Also, not having executable specifications (tests) often leads to Overdebugging, which is a completely different story.

So next time, dear Reader, if you are challenged with a problem to solve, pretend to have "wishful amnesia", temporarily forget about implementation details and ask yourself:

Question 🤔

What do I, as a consumer, need? How can I specify that?