Test-driven development is hands-down the best thing a programmer can do for their sanity, productivity, and happiness. TDD forces you to think about code in modular and reusable chunks. It promotes architecture-first design, lets you change your code easily. If you try to search the web for “disadvantages of test-driven development” you will find some stretched arguments that boil down to: TDD won’t miraculously make your architectural decisions perfect. But it will make them better.
So, why don’t we all write test-driven code? Let’s see.
I recently started working on a project that uses Python for a REST backend and assorted services. I read a lot about React, so I decided to use it for the frontend. I know Python inside and out and have used it to write a plethora of software. I understand everything there is to understand about REST APIs, message brokering, threading, and everything else I will need for the backend. On the other hand, I have never used React before.
My approach was simple enough. I documented all the data structures, states, and flows that I need. My plan was to first implement a backend with 100% code coverage. With a backend in place, I would implement a React frontend to get a pretty user interface.
I rolled up my sleeves, turned the Pomodoro timer on, and started coding the backend. Before long, my code was filled with pyunit tests and Python methods waiting to be implemented. All I had to do was fill in the blanks. I started implementing my methods. One by one, my tests turned green. All methods were tested. All REST endpoints were documented and covered by tests.
What a glorious day it was! I implemented a bunch of user stories. Well, their backend part at least. It was truly a blissful moment when I git push-ed the last change.
Tomorrow morning started just as optimistic. There was one tiny problem. I never really used React before – frontend is not my forte. I chose a component library (MaterialUI), which was another layer to learn and figure out on top of React.
I simply couldn’t start with tests first. Firstly, I didn’t understand what my outcome should be. For example, for one view, I knew I needed a table with some data and some buttons in it. I tried to reason this down to two things: let’s first test if the data is correct and secondly if the buttons work. Sounds straight, right?
It wasn’t straight at all, to me, at that time. I had no idea how to test if the data is correct because I had no idea what kind of output Material-UI will provide. If I am using CSS selectors to find, for example, the second column of the second row, how do I know which selectors to use? If I wanted to test if the pagination works, how do I mock a page change? Do I even need to mock it, or is there an infinite scroll? Besides, would I be testing my code or Material-UI library this way?
Although it sounded easier, the functioning of buttons was even harder to figure out. I kind of understood that when a button is clicked, I will need to persist data on the backend by calling some REST API and then reflect it in the view. But I didn’t understand at all how I will accomplish this. Do I need to listen to a state change of a parent component or try to spy on props of my component? How do I even organize the hierarchy of React components? Which hook is the best one to use for my data structure?
If you are reading this and you are a React guru – you are probably laughing at my questions. And in retrospect, I am laughing at my ignorant past self.
But, whenever we are faced with a new paradigm in programming, we have to explore it, and play with it, and make a lot of mistakes. We make the code run without knowing how we did it. We have an aha! moment one minute, just to understand the whole approach was nonsensical a few minutes later.
And in all of that exploration and trial and error, it becomes impossible to write tests first. If you want to write tests first, you have to be sure what the outcome will be. But if you have no idea what you want to accomplish, because you don’t understand the paradigms you are working with, then how can you define the outcomes?
By the time we are done and we have some grasp of the technology, we also have a big chunk of untested, badly structured, and assorted technological debt.
How should we deal with this? Well, simply. Once we understand the paradigms and gotchas of the new technology we just delete all of our exploratory code, no matter how feature-rich it is, and rewrite everything, tests-first.
There is a slight problem with this approach, of course. In my case, writing the frontend should’ve taken me a week, but it took me three weeks instead. And now I needed to spend another week on it to fix it.
So, I just did what we all do. I left the code as-is and made a pinky swear that all new frontend code I write will be written with tests first. About half a dozen user stories later, I am really still building upon the existing, messy, code-base and not writing any new code. My messy code-base is getting bigger and messier by the day, but as soon as I start writing a new, independent, module, it will be test-driven. I swear.