Skip to main content
January 19, 2023

TDD, Asimov, and the Zeroth law

Introduction

In this post, we will quickly introduce Test Driven Development (TDD). We will explore some things we have learned on this passionate journey. We hope you enjoy it. Who knows? Maybe you will join this dark cult.

What is Test Driven Development?

TDD or Test Driven Development is a programming technique consisting of writing a test before the “production” code. This is done by repeating three steps:

  1. Write a failing test for the code you want to develop.
  2. Write the minimal code to make the test pass (go to green).
  3. Refactor the code, maintaining the test on the green.

After a long time practicing it, Uncle Bob describes three simple rules for writing code (please read the linked article; it is exciting, I promise)

The three rules are:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write more of a unit test than is sufficient to fail; compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

There are many exciting things in the three steps of TDD and Uncle Bob’s rules. Come with me to see some of them.

TDD Cycle

The first test, yeah, but what do I test?

It is a common question when you want to start this new life. Answering the question could be simple: Test the code you want to add. Do not laugh, please, and try it.

The TDD cycle may seem easy, but I always find that the first test on a project is the hardest one. It does not matter if we are in a greenfield project or jumping into a project that has already been started. The first test is the hardest because we need to create the scaffolding necessary to write it; that is, complete the tests project where the tests will ‘live,’ folders, infrastructure, etc. But once you get it, adding another test will be extremely easy.

Remember Uncle’s Bob sentence: “Compilation failures count as failures.” Do not panic. You are in the cycle. Finish the scaffolding, and go to the next step, which should be your “first test.”

Write a test that instantiates a variable of a class that does not exist; that is a failing test; now create that class, give it a name, the test gets green, and your system is “working.” Directly invoke the method where you want to add the logic; it does not exist yet, perfect! Your system is failing, but a few seconds ago, it was working. Use your powerful IDE to create that method that does nothing … excellent; it is green; the system is working again.

One more time, do not panic; you have made some decisions and are getting feedback instantly: Does that class name fit? And the method name? The parameters? The returning types. Does it? Great!!! Doesn’t it? Better. You have feedback that something is not so well; maybe you need to rethink some names …

Wow, hang on a second…

You do not have to generate a system version, deploy it to production, let the user run it, and see that something is wrong… You have instant feedback in development time that something is not going well. Do you think it has been a waste of time? With that learning, you can step back and try another thing, but this time you have more information, so go back and start again; with only two or three lines of code written, you earn knowledge and more likelihood of being successful next time.

Make test pass

At this point, you have a failing test. Typically, the test code should look like this:

– An instantiation of the class you want to test (Arrange)

– Invoke a method (Act)

– Check if the logic is doing what we expected (Assert)

Now it is time to write the code that makes the test pass. These are just the very few principles that make the test give. An example could be just a “return true.” Do not be afraid; we did not end the cycle, and the refactoring step has not been done yet. And as TDD is an iterative process, the next test you write will make you modify the “production code,” so do not be afraid to make some “leap of faith” as you read before in Uncle Bob’s article, your code will be running without errors “a minute ago.”  That is called “do baby steps”; we will discuss that later.

Try Assert-Act-Arrange instead of Arrange-Act-Assert

We have just seen the typical pattern to write good tests, Arrange-Act-Assert. What if we do it in the opposite flow?

Assert

How will you check that the code you want to test goes well? Maybe a property? A method returning value? Think about it and set the assert. You are making decisions and getting feedback if they are worthy.

Act

Run the action, the method you will invoke to make the code run and do its thing. You already have how to check if it does it well. Here you are deciding how to gather parameters, function names, etc…

Arrange

Instantiate your object with the needed dependencies.

On every “section” of the test, you are developing, designing, and experimenting with the code you have in your mind; it is not written yet, but you have various mechanisms to check if all you have in your mind finally fits in the system. And if something looks weird, or you find that you missed something, hurray., step back, and try again. You are like Doctor Strange with the Eye of Agamotto.

TDD is not about velocity, but frequency: on feedback, running your code, checking something is broken…

Refactor the code added

Ok, now we have passed a test from red (fail) to green (key). So, it is time to see if our code is easy to read, remove duplications, and look for bad smells. This step used to be, for me, more complex. Just because we must make those changes considering the following:

  • Not breaking the test, we write neither another test.
  • Not implementing more logic than necessary.

The feature we want to code still needs to be finished, so we jump again to the first step and create another test until the part is finished.

Simple, don’t you think? Spoiler: It isn’t. It is easier said than done.

Learnings

The steps seem easy, but there is much hidden knowledge behind them. Let me show you what I have learned from those laws, that cycle, and practicing it repeatedly.

No Silver bullet, but tracer bullet

TDD is not a silver bullet; I find it more like a tracer bullet, as described in “The Pragmatic Programmer” [2].

It is a way to make the famous “baby steps” and get feedback if we are going the right way. When we find that is not the way, we step back and give another one in another direction. “This is the way,” a Mandalorian will say.

No, really, what should I test?

If you are not sure to make a test for the code you want to add … (that’s not the way).

A good start could be in the way of fixing a bug. Once you have found why a bug happens, generally after some debugging time, do not immediately fix it. First, try to create a test that reproduces it and focus on that, see how you need to instantiate the class, set properties, and invoke the method … It is common to find that it is challenging to instantiate a lesson; some dependencies annoy you. “Welcome to my world, Neo.”

Take a meaningful note of the test you want to create. Once you are done, try to decouple that dependency and do your magic, extract that dependency to a method, then to a class, and so on, and be imaginative. While doing that, write a meaningful note to test the code you refactored. The point is to maintain focus on the current job.

Karate Kid and Nobel prices

Do you remember the “Karate Kid” film? Do you remember Mr. Miyagi telling “Danielsan”? “Wax on, Wax off”? It seemed easy to do, but for “Danielsan,” it was nonsense for learning karate. Practicing TDD is very similar in the first tries: if we persist with that practice, we will sooner rather than later see the truth behind it.

Have you ever heard Kent Beck’s quote: “Make it work, make it right” [3]

In software development, we know that the first code we write to solve a problem is not usually the best solution. We are used to making iterations, and the code is modified each time to meet the desired behavior or feature.

Daniel Kahneman [4], a psychologist and economist who Nobel prize in Economic Sciences, talks about how the human brain works in his book “Thinking, Fast and Slow.” The human brain has two modes, fast and slow. The immediate mode is designed to answer questions and situations quickly; the slow mode is the analytical mode.

The fast mode is an evolution artifact that results to reduce resource consumption to make decisions that allow an individual to survive in some situations. You need to hide from danger when you hear a noise behind a bush. But nowadays, you know that it is extraordinary that a lion is chasing you; that’s the slow brain mode.

There is a relation between Kent Beck’s and Daniel Kahneman’s words. While we are coding, we try to make it work at first, and after that, we try to do it well by refactoring, separating responsibilities, creating abstractions, and so on.

TDD helps us stop that fast mode in our brain and make the slow mode act from the beginning. By making the test first, the slow mode brain takes control; we start to think about class names, method names, parameters, how we will check the code, make what we need to … and so on. The result is a code that not only does what we need but a code readable by a human being.

Greenfield, Legacy project and wasted time

When we start a new project, we can develop features very quickly, fast brain mode is ON, and we are establishing good foundations because we will avoid making the same mistakes we did on the last one. It is a greenfield; we do not have time for tests, need to close features ASAP, and have a reasonable velocity. Suddenly, one day, the value-adding velocity decreases, we are not closing as many features as we used to, and the greenfield becomes a legacy project. Some team members start arguing to stop and start it again. Wow, a déjà vu, what did we do wrong this time?

“Make it work, make it right” (wax on, wax off).

Fast mode brain is good at making things work quickly, but did we allow the slow mode to try to “make it right”?

That’s where TDD laws make sense. They allow slow, methodological brain work. In “TDD by example,” Kent Beck recommends creating a list of tests you plan to do before starting, and I didn’t get the benefits of that for some time. That simple step has two goals:

  • Wake up your slow brain and start to have feedback from the very first thoughts and decisions about the solution you are designing.
  • Focus. You start with a list of tasks that get you focused only on one test at a time. Let me explain this point a little more in the next section

Focus. Zeroth law

In a greenfield project, it is easy to have all “logic” in your mind and develop features quickly, as the system complexity is manageable at the beginning. However, with time, that complexity rises exponentially.

Having a list of tests you want to add on paper allows you to focus only on the current task, as you don’t need to worry about the rest of the system. It was already tested, and one minute ago, the plan was working [1]! If you break something, it is only in the code you have just written.

What we get from testing in this way is a project that is always a greenfield project. All was working a minute ago, so the fear of deploying on a Friday evening goes away too. Wow, that quickly scaled Juan María. Ok, you have the confidence to deploy on Friday, but DON’T DO THAT.

That’s what I call the “zeroth law” of TDD, as the Asimov one [5]. Before writing a code for the test, write the paper test.

Is this faster?

I have no doubts, but I have no proof… I cannot count the times I have NOT had to stay until 3 am solving a production bug …

What do you think is faster, adding new features too:

  • A “greenfield project” with the previous features unit tested as the result of applying TDD
  • A project that has no tests to check if something has been broken

It is exciting to think about it.  Yes, but we need to be on schedule; we are running out of time. Time is so precious; that is the reason we should test our code. It is a common mistake to say, “We do not test because we have no time.” When you start trying, you will soon realize it is the opposite: “We DO test because we have no time.”

Maybe a picture will help with this:

picture about tdd cycle

Summary

I hope you find this post helpful and enjoy it. It does not matter if you practice TDD; having your code tested is essential. And yep, it does not matter if those tests are good. I prefer a terrible ordeal run on every integration cycle (CI) to a perfect test that needs to be written or run.

That’s the first step; after some time, I am sure you will evolve to practice TDD; it is just a matter of time.

Bibliografía

  1. http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd
  2. https://pragprog.com/titles/tpp20/the-pragmatic-programmer-20th-anniversary-edition/
  3. https://es.wikipedia.org/wiki/Kent_Beck
  4. https://en.wikipedia.org/wiki/Daniel_Kahneman
  5. https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
Author
Juan María Laó
Software Development Engineer