Why We Shouldn’t Use TDD

I know you’re thinking I’m going to argue that we don’t need to test our code, but that’s what this is about. I’m simply saying that we should not be doing the process of TDD.

Whenever I create an argument I always use definitions, this helps everyone stay on the same page and we know what is being discussed. This is no exception so we’ll take some time to define what, exactly, I mean by TDD.


Defining Test-Driven Development

If we go to Wikipedia we get the following passage summing up TDD

Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: requirements are turned into very specific test cases, then the software is improved so that the tests pass. This is opposed to software development that allows software to be added that is not proven to meet requirements.

Then we get some steps of TDD, which are as follows:

1. Add a test

2. Run all tests and see if the new test fails

3. Write the code

4. Run the tests

5. Refactor the code

Repeat (this isn’t a step according to the Wikipedia page, but it happens at the end)

I think we can all agree based on this that TDD is the process of writing test cases for code that you have not yet written, to ensure that the code does what you think it does.


What is wrong with TDD

My first big issue with TDD is that it tells you to write tests first. How are we supposed to write a test for code that does not exist? The answer is that you cannot effectively test code that has not been written yet. You end up writing your code to satisfy your tests rather than writing code to build your product or accomplish your goal.

Let’s use an example. We want a function that takes in an integer, which represents a percentage grade that a student has received and tells us what letter grade that would equate to. Here is an example in C#:

https://gist.github.com/shadow1349/82504d5aa6c3a11327ee726b9b1a7bf1

This is a simple example with some obvious bugs. We can see what the requirement is in the function’s summary and we can see how the code does not satisfy the requirements.

It is obvious that if the grade were 90, for example, our function would return ‘F’. This does not satisfy the requirements and it should be clear to any developer that we just have to add in some “or equal” signs and probably throw an exception when the grade does fall into one of these categories it’ll be good to go.

Back to the main point, the question is, did we need to write a test for this code before we wrote it? I don’t think we did as there would have been no value add to do it at that step.

To explain more let’s take a look at what the tests might look like:

https://gist.github.com/shadow1349/feda163e28b59718348972214d69e72b

These are just 2 test cases, but you can gather what the other ones might look like (exactly like these ones). So what would be the purpose of making this before we wrote the code? I suppose a better question is would this have added any value by writing this test first? Some would say that it adds value because then you would have caught the fact that your code would have failed.

I argue that the tests would have a similar value whether you added it before you wrote your code or after your entire codebase is complete. The reason I make this argument is that this would not have changed the fact that a test would have caught this problem at any stage in development (preferably before pushing it to prod).

I made sure to bold the word similar because I actually think adding your tests after your code is complete adds more value than adding your tests before the code is complete. I also believe that it is egregious to add a full test suite to test code that doesn’t exist.

I think of it this way because any time we write code we are trying to solve a problem or reach a goal. All the code we write should be written in order to solve that problem or reach that goal NOT in order to satisfy some test cases. I think of it like trying to proof-read a paper before it’s written.

As developers, it seems as though we are constantly trying to push more code faster. Come out with better features, refine the features that exist, do it faster. I think that following the process of TDD will largely hinder the rapid development of features and ultimately stop you from being as innovative as you can be. When you write tests before code it can be so easy to lose sight of what you’re trying to build and the goals you’re trying to accomplish. This will lead you to create features and code that really misses the mark in many respects. Too often I have seen people re-write sections of code, not to make the code better, but because it will be a little easier to test. At this point, we are taking away a lot of the value of that code because it is no longer written to solve the problem it is written to test.

Another problem I have is that TDD takes away from a lot of common sense. In my simple example, I don’t even think tests would have been necessary. I think that a good developer would have been able to look at the requirements given and use their power of common sense to pick out the bugs. In every environment, I’ve seen TDD used as a way to combat common sense as opposed to empower it. When we write tests for such trivial pieces of code we’re really just wasting our time and trying to make ourselves feel better about the code. We tend to use this process as a crutch to lean on so we don’t have to use some really basic common sense.

Lastly, TDD does not stop bugs from being shipped to production. If you make software it’s going to have bugs no matter what you do. I don’t think TDD necessarily promises that you will never have bugs, but I think a lot of project managers tend to think of it in that way. Let’s be very honest, nothing is going to stop you from having bugs in your code, it will happen now and it will continue to happen until the end of time and TDD won’t solve that.


If not TDD then what?

I mentioned at the start that I’m NOT trying to say that we shouldn’t test our code. What I am saying is that we have to re-think how we test code and how we think about testing code. I think that we need to start viewing tests as a way of proof-reading code rather than defining code. I don’t think that we should spend time refactoring our code in order to test it, I think we need to refactor our tests in order to test the code. As developers, I think that we should constantly educate ourselves about the code that we write. I think that we need to spend more time making sure that we write good code that solves problems rather than code that can be tested easily. When we focus on using our code to solve problems we will be able to ship better products much faster. I think I’m going to call this paradigm Software Driven Solutions.


Software Driving Development of Solutions

What does this actually entail? Over the years I have been a part of many organizations that I have disagreed with about the way they think about writing code. Until now I don’t think I have had an answer to the problems I have pointed out, but I think I do now.

This is not a rigid framework, so please don’t turn it into one. This is a very loose set of guiding principles that you’re meant to take and mold to your project so it fits perfectly for you.

1. Remember why you are writing the code

No matter what the goal of your software is, keep that goal/problem in mind while you’re creating it. Ask yourself if what you’re doing is really the best way to solve that problem. This does not mean you have to have a meeting or even tell anyone else about it, this is meant as self-reflection on your part as an individual. This can take as much time or as little time as you need. Do this continually throughout the development process to make sure you don’t lose sight of why you started a project in the first place.

2. Don’t over-complicate things, keep it as simple as possible

It is very easy to go down a rabbit hole of convolution when we write code. This makes our code difficult to read and difficult to debug, which can really hinder team performance. Try not to confuse laziness with simplicity, laziness is sloppy simplicity is elegant. This also does not mean that things cannot be complicated. Sometimes when we write code things get complicated, and that’s ok for it to be complicated. The key here is not to more it more complicated than it needs to be.

3. Document your code

You don’t need to write a book on a single function unless it is absolutely necessary. In general, try to keep the documentation of your code short, simple, to the point, and don’t make it too cluttered. Here is an example using the function I demonstrated earlier:

https://gist.github.com/shadow1349/e6bac2f2fd3c00d66da5493af8190b58

Documenting our code like this will help us remain self-reflective about the reasons we’re doing things in a particular way and it will flow naturally when you’re writing code without taking up a lot of your time. This will help you think out loud and help you define your thought process while you write code. This will also help your team figure out what you were thinking while you were writing code and help them understand your code more intuitively. Lastly, this will help you catch silly little mistakes in your code. By taking one minute to go through and document your code you will have caught that bug and helped your teammates avoid doing the same thing, while simultaneously explaining the code in an easy to read and intuitive way. If you take away one thing from SDS, let it be this.

4. Proofread your code

This is an important step to do, and it can take many different forms.

  1. The most popular form this can take is adding automated test cases to your code. Doing this is a nice solution because those tests can be run by everyone on your team, and can easily spot failures, but be careful not to use your tests as a replacement for understanding the code fully.
  2. Running and interacting with the code. It is always important to run your code and interact with it in the way you intended it to be used. It is always important for our code to work in the way it was intended, and it is also important to acknowledge that it may not be used in that way. If you can figure out ways it works in ways that are not intended, that is an added bonus.

These are just 2 common examples, but you can really do whatever works for your team. This will help you prevent code failure, bugs, and other mishaps that may present themselves along the way.

What now?

I would like to continue to expand SDS style development with the help of the developer community. I want to make this something that is for developers who are out there working in the market today by developers who are out there working today. I think that a lot of software development paradigms have lost touch and I want to try to change that. If you have any ideas, I will be starting a Github site to define more of the thoughts and beliefs about this and put in a place where everyone can contribute to it. Once I have something up and running I will create another post, so please stay tuned!

If you’ve enjoyed this article I’d love for you to join my mailing list where I send out additional tips & tricks!

If you found this article helpful, interesting, or entertaining buy me a coffee to help me continue to put out content!

Previous
Previous

Why you should choose Angular for your next front-end project

Next
Next

How Agile Fails in Practice