TDD

Table of Contents

1. TDD

1.1. I think fast feedback loops are more important that “test first”

Build code to test what you want to achieve and code that undoes that change, like in Climbing Mount Effect
Prioritize quick feedback over testing for correctness

1.2. Separating the wheat from the chaff of TDD. | by Victor Ronin | Geek Culture | Medium

1.2.1. Separating the wheat from the chaff of TDD.

The root of the practice is a five-step activity (taken from here)

  • Add a test
  • Run all tests. The new test should fail for expected reasons
  • Write the simplest code that passes the new test
  • All tests should now pass
  • Refactor as needed, using tests after each refactor to ensure that functionality is preserved

1.2.2. Add a test

1.2.2.1. Pros: You are forced to think about testable interfaces

Many people who are not familiar with TDD try to write production code first and after try to retrofit tests only to find that their code is not easy to test (and decided to skip it altogether or have lower test coverage because of that). Since you write a test first, you have to think about your code’s interface, so your test will work. This flips the problems on its head.
To give you an example. Let say you are writing a calculator. You added a method like that (before writing a test)

    def addNumber(uiDelegate, value)

And now, you come to a place to write a test. Oh.. crap, I can’t create uiDelegate, because my tests are executed in headless mode. Oh.. And the class contains an old state. Ugh… How do I write a test for it?
On the other hand, if you start with a test and write a test for your (currently imaginary) code. It becomes self-evident how you want the interfaces to look (vs. how they accidentally worked out).

    calculator = Calculator()
    calculator.addNumber("1")
    assert calculator.getResult() == "1"
1.2.2.2. Pros: Baby steps

A lot of engineers tend to overthink. They look at the task, which has hundreds of possible test cases, and got into analysis paralysis, trying to figure out all permutations, all details, all cases BEFORE writing any code.
This forces you to think about one problem at a time. Each time you are working on literally one straightforward test (which most likely tests just one method).

1.2.2.3. Pros: Help with class-level design

You almost don’t need to think about class-level design. It happens for you for free as you write your tests and refactor them.

1.2.2.4. Cons: You can’t test your way out of not knowing the solution

I remember reading an article about how one of the proponents of TDD was trying to write a Sudoku solver using the TDD. It was quite interesting to read. The guy spent a lot of cycles trying to find a solution via tests. If he concentrated on a solution (ignoring tests), it would take him way shorter, and he had plenty of time to rewrite it afterward to make it testable.
«Maybe this is why it doesn’t work as well in data science, there is a considerable discovery phase»
If you use it fanatically (purism), you may get the impression that TDD will get you out of any trouble. This is not true. There is a considerable discovery phase (a place where you don’t know what you need to do, how you need to do it, what kind of limitations you have). None of these questions are answered by TDD.

1.2.2.5. Cons: It can’t help you with a high-level architecture

TDD doesn’t have to use unit tests. However, they come hand in hand. Unit tests are heavily concentrated on testing specific classes and oblivious for a higher-level design. As a result, you can very efficiently implement each class while having a terrible modularization and problematic higher-level architecture.

1.2.2.6. Cons: You can test yourself in the corner.

Again, if you are eager to think about things one in a time, you can implement several dozens of tests to find that your whole approach was not well thought through, and you have to start from scratch.
This approach (of solving simple problems first) works well on large but simple tasks and works terrible on more compact but very complex tasks.
The common theme for the last three items — TDD claims to remove a lot of up-front design. However, if you go this route too far, it can easily backfire.

1.2.3. Run all tests. The new test should fail for expected reasons.

This sounds trivial. Something, like Duh… Sure the new test will fail since the production code is not changed yet. However, there is deep wisdom in that.

1.2.3.1. Pros: Catch incorrectly implemented tests

This is especially true when you are trying to fix a bug. You may tend to fix a bug first and after writing a test. You run your test, and it works. One second… What if a test was written incorrectly (not reproducing origin problem)? In such a case, the test passing doesn’t indeed check whether the bug is fixed. It checks something else, but not an original bug.
This could be less of a nuance for new functionality but critical for bug fixes.
This feels rare, but I stumbled on this enough times (and spent probably a day each time after that banging my head against the wall) to realize the value of this easy and quick check.

1.2.3.2. Cons: You start believing in test infallibility

If tests drive everything, it’s too easy to start relying on them too much. If your test is failing, it means that it caught some bug, it doesn’t mean that it caught a bug which customer saw. If your tests are passing, it doesn’t mean that everything is great.
Automated tests are only a piece of the puzzle to get high quality.
I think purists think too highly of TDD (that it’s pretty much infallible). For example, people who just write unit tests will rarely believe that unit tests are all their need.

1.2.4. Write the simplest code that passes the new test

1.2.4.1. Pro: Baby steps again

You can concentrate on literally satisfying one test instead of having to think through the full implementation of a method (or class).On top of that, you are encouraged to do it the simplest way possible.
This one with a previous one reduces immensely cognitive load. It’s an amazingly freeing feeling to think about one tiny thing at a time versus a hundred.

1.2.5. All tests should now pass.

There is not that much to comment on here. Obviously, after making changes to the production code, you need to ensure that you didn’t break anything. So you have to rerun the test.

1.2.6. Refactor as needed, using tests after each refactors to ensure that functionality is preserved

The resulting code created by a first pass could be reasonably ugly. It is supposed to be the simplest one, which could easily have duplication/hardcoded things. As a result, it needs to be cleaned up.

1.2.6.1. Pros: Refactoring is a first-class citizen

People often are uncomfortable with refactoring, especially if there are not enough tests (when TDD is not used). As a result, people may be afraid of refactoring code after they manually tested their code.
This normalizes refactoring and makes it a repeated and expected activity (and well supported by an excellent test coverage).

1.2.6.2. Cons: Refactoring is a tool (not a goal)

Good engineers like to polish. Unfortunately, I saw over my career a couple of runaway cases (including me doing too much refactoring at one moment). Polishing an application could be addictive, and people get stuck doing too much refactoring (while there is a bigger fish to fry).

1.2.7. Other thoughts

Some things can’t be tied to one of the activities but are more related to the whole process.

1.2.7.1. Pros: You build an excellent test coverage out of the gate

I see tons of projects struggle to have decent coverage (higher than 80%) and always try to catch up on it. TDD forces your hand to have a test coverage very close to 100%

1.2.7.2. Pros: No need to debug

Working with baby steps is so simple (both to write test and production code) that you may not need to debug things. It’s fantastic to see an application coming together without the tedious setting of breakpoints and figure out why some variable isn’t what it should be.

1.2.7.3. Cons: You may not have covered all cases

You will be forced to cover all positive paths. And most likely, you will cover some negative tests. However, as this practice is more iterative, there could be less emphasis on enumerating all possible test cases.
This is especially problematic regarding covering “blind” spots. You are working on the test and production close so close in time that if you haven’t thought about something, you are more likely to miss it and move on to the subsequent tests (for the next class).

1.2.7.4. Cons: quality is made by design, not testing.

This wraps a lot of my concerns regarding architecture, thinking through all tests cases, and on on.
Quality is way more than just having 100% test coverage. It’s about architecture, it’s about consistency, it’s about predictability. None of this is generated by TDD on its own.
BTW. As I mentioned above, It’s less of a problem with TDD, and it’s more of a problem of TDD purists, who use it solely (while ignoring other necessary practices).

1.2.7.5. Cons: Requires serious buy-in

It’s hard to have half of the team doing TDD, and another half is not doing it. Possible, but it’s hard. A person doing TDD will constantly be on the short end of a stick.

1.2.7.6. Cons: Testable code quite often have a specific smell

Testable code heavily uses Dependency Inject and Inversion of Control (in a lot of programming languages). You get used to it, but it’s getting harder to understand the code when DI/IoC permeates it.
This applies to just writing unit tests (even without TDD). However, TDD forces you to write tests for everything, which makes it even more pervasive.

1.2.7.7. False Cons: Time spent on writing/maintaining tests

Please notice the word “False.” A lot of people throw this around. I totally disagree with that. Time spent on writing tests (before or after writing production code) will be paid back by a hundredfold by not hunting simple bugs forever and ever. This is regardless of whether it’s written in TDD style or written after production code.

1.2.8. Wrapping up

I think TDD is a great thing to really try (give it at least a couple of weeks and stumble, stand up and find a good footing).
However, I felt that TDD purism was a severe obstacle to broader adoption. There were too many Cons (caused by purism vs. practice itself).
I felt that there are things that are great and need to be used daily — huge emphasis on tests, usage of baby steps, and making code testable. And the things which don’t work well like underemphasis of high-level design, quality beyond writing tests, and just thinking things through.

1.5. Rumbling about Test Driven Development

https://www.reddit.com/r/programming/comments/ua6hr4/rumbling_about_test_driven_development/?utm_medium=android_app&utm_source=share
Writing tests before writing code is using the code to break down the task instead of using paper

It seems to me that the main benefit of TDD is not that you have code covered with tests before it’s even written, but that the pre-defined set of conditions focuses the developer on solving a particular problem

From reddit:

The cycle we use now I call TPD, Test Protected Development.
The cycle for generating a new feature follows this pattern.

  1. Get the general idea of what is required,
  2. Using available Libs and quick and hacky development make prototypes, iterate on these to validate ideas from 1 and know some of the original unknowns.
  3. Write the tests that basically capture the desired behaviour found via prototypes.
  4. Throw away hacky code, determine what relies on external Libs and figure out if you can also throw the Libs away and can replicate and maintain yourself.
  5. Clean code the solution to meet the tests determined in 3.

So far it’s producing better outcomes for the stakeholders because the prototype phase gets closer to what they actually want, the tests better capture what successfully passing the test means, and the code is nicer.

1.7. “Lean Testing or Why Unit Tests are Worse than You Think”

“Lean Testing or Why Unit Tests are Worse than You Think” por Eugen Kiss
https://link.medium.com/vxBRbiXld9

1.8. “Software Development Isn’t About Unit Tests”

“Software Development Isn’t About Unit Tests” por Chris Fox
https://link.medium.com/tHuFn5GHl9

1.9. Is TDD Dead?

A series of conversations between Kent Beck, David Heinemeier Hansson, and myself (Martin Fowler) on the topic of Test-Driven Development (TDD) and its impact upon software design.

Author: Julian Lopez Carballal

Created: 2024-10-21 Mon 09:32