Sometime in the early 2000s, a film maker named Nagesh Kukunoor (the poster boy of indie Indian cinema), made a film called Bollywood Calling. It’s a comic and sentimental tale of a down-on-his-luck American actor, Patrick, who comes to Hyderabad to shoot a film. Om Puri essays the character of the producer in an amazing performance of a shady but good at heart wheeler dealer type. Now from the start of the film Patrick has been asking for the script of the movie but he never gets it. On the day the shoot wraps up, Om Puri comes to him and the following scene ensues
It’s a sweet film. You should watch the whole thing!
So yeah, I feel like Patrick when I ask devs to write tests and they say they’ll do it ‘once the feature is done’. Haha - once the feature is done and is stable you don’t need tests! You need them when the code is changing.
And I see this response over and over again and I’ve come to believe that it’s for two reasons
- a fundamental misunderstanding of what the tests are for
- a belief that writing tests is hard
Over the years I have introduced the practice of TDD in many teams and the script is always the same - devs, terrified of testing their own code when they could just get some QA to do it, start to raise all manner of objections to TDD. Then they try it. Then they fall in love with it and are forever improved as programmers.
If you’re an Engineering Manager trying to introduce TDD into your team, this post will help you navigate the objections and guide your team to TDD zen. If you’re a programmer wondering what all the fuss is about then this article is for you also.
The thing is TDD is only tangentially about testing. There’s so much more going on here and words do not do it justice. It can only be experienced. The act of ‘covering a feature with tests so that it is stable in production’ is not TDD. TDD is about discovery, it is about architecture and it is about refactoring safely. Writing tests after a feature is done is like writing the script of a movie after it has been shot. You’ll test what you get. The converse is also true - you’ll get what you tests.
Disclaimer - Many of you are going to scoff at this post. If you’re a clojurist or Haskeller I have no doubt you’re a better programmer than me and this post is not for you. But I have seen TDD transform so many programmers from ordinary to extraordinary that in my opinion it remains the most reliable way to scale up a teams skillset.
Why do TDD?
Most everyone hasn’t tried TDD but boy oh boy do they know all the reasons to not try it. There are many tactical reasons given for not trying it but the most dangerous ones are the philosophical reasons - TDD solves nothing, it is dogma, it is only narrowly applicable. Ok, here’s my take on it.
In any case, there are two levels to this question -
- why have automated tests?
- why write the test first.
Let’s answer the first question first.
The people who create the system are responsible for its quality.
That, dear developer, means you. If your code comes back from QA with dozens, if not hundreds of bugs, than I’m sorry to tell you this — you are not a responsible developer. And EMs please take note, if your devs are creating code like this, it’s on you.
And as a developer if you’re testing your application by hand then do you even understand computers? As someone once said — let computers do the work, use humans to drink beer with. If you’re not automating your testing you’re never going to be able to deliver a zero-bug build to QA.
In order to correctly evaluate whether TDD works well or not, one has to take a step back from the idea of tests as a test of the correctness of the code and see it instead as a very fast feedback loop. The thing about unit tests is not so much that they tell you whether your code is correct or not, it’s that they do it in a matter of seconds. In some sense, most of you are already testing your code - you’re just doing it slowly and making mistakes while doing it - ie testing manually. Tell me you’re not hot-reloading your front end or have a console open to run the code you just wrote? TDD is nothing but putting this in a script so the computer can do it for you. And the pickup in speed that the computer brings changes everything.
You see, very few of us are smart enough to write good and correct code at the same time. I certainly am not. With an automated test suite you unlock the most powerful predictor of good software — the ability to refactor fearlessly. And once you can refactor fearlessly you can separate the writing of good code from the writing of correct code. First make the code correct and then refactor fearlessly as the tests guide you step by step to the correct answer. Make it work, make it right, make it fast.
Without the ability to refactor fearlessly, your code is going to suck. The biggest predictor of the success of anything is its ability to adapt to changes. If your code can’t adapt it will not survive. Being able to change code fearlessly can make the difference between the life and death of your code.
This you will not understand until you experience it. If you have 3000 line classes, all your objections about TDD being dogma and not a silver bullet are just lines you’re repeating without understanding what they actually mean.
You see, code is like a living thing. It can only be known to exist when it is perceived, ie used by some other piece of code. And these other pieces of code help to shape it into the correct configuration that makes it a good fit for the environment. Having a piece of code be “perceived” by tests brings it to life and makes it fit for its environment in a safe space. Not just that
Common Objections to TDD
If as an Engineering leader you try to get your devs to do TDD, you will hear something like the following.
It’ll take longer if we write tests.
It’s a common objection based on a simple and completely incorrect logic - tests are code and more code takes longer than less code. But the words don’t mean what you think they do. What’s actually being said is that I don’t know how to write tests and learning to do TDD will take longer than just writing the code. And here, EMs, the only solution is to make time and space for learning TDD.
The thing is people mistake the learning curve of TDD for TDD. Yes when you’re learning TDD it goes slow, but that’s true for everything. When you were learning to ride a bike, it didn’t go very fast did it? Not only did it go slow you probably fell down and hurt yourself a few times as well. But you didn’t give up. You know why you didn’t give up? Because you could see your older siblings happily tooling around town on their bikes and you knew that you would eventually figure it out and unlock new levels of fun and freedom in your life. This sort of demonstration is not easily available for TDD. I mean half of conference talks are about testing, no serious library comes without tests, no PR to any of these is accepted without tests and no language sees adoption without capable testing infrastructure. But yeah, let’s for a moment ignore all of these and agree that the lived experience of TDD is still unfamiliar to most programmers. You will have to engineer a win for the team - a taste of life with TDD.
Once a dev is fluent in TDD, the code and tests get written faster than just the code. And it’s not just because the automated tests run faster than your slow-ass hand testing. The tests reduce the cognitive burden of writing code. You no longer have to pretend to be a computer and understand what your code is going to break elsewhere. The tests will just tell you. And this laser focus on just making the current test pass frees up the mind. The fast feedback loops help to put one in a state of flow and the correct code just magically appears from following the path laid down by the tests.
Imagine if a school kid comes late to school and when asked why they’re late they say - well you see it took me longer because not only did I have to come to school, I had to ride the bike as well. What would the Principal’s reaction be?
Yup. That’s the same reaction I have when you tell me it’ll take longer if you have to write tests as well. The tests are what make you go faster!
Tests are complex to write
That, my friend, sounds like a you problem, not a TDD problem. In fact the very first hint that your code is not good comes from this - tests being hard to set up. What it likely means is that your code is poorly coupled and you’ve not extracted the correct domain model out of the problem to allow your units to function as units. Some more work on SOLID principles (if you’re an OOPSer) will definitely help here.
Here’s a story about removing complexity from tests. The mobile team at shaadi.com were rewriting the profile listings page. I had asked them to use TDD to do it. The first iteration that came back were some slow-ass UI tests with all sorts of Dependency Injection going on, test setups that were slow to write and execute and a general feeling of frustration. I asked them to read more on TDD, especially understand what a unit is. Several iterations later they realised that what they essentially needed to get right was the interaction model between the User and the Profile - ie if a User is looking at a particular Profile, what all interactions are available to them? Can they see private pictures? Can they send a message? Are they blocked and so on.
Once this penny dropped they went through each and every permutation and combination of Free User, Paid User, Male, Female, NRI and so on and drew up a spreadsheet more comprehensive than any PM had dreamt of. They used this sheet to populate a set of fixtures, converted them into tests and went to work. In the end they ended up with one function that given two users data would return an interaction model with the correct permissions. Seeing the whole domain collapse into this one function blew their minds. The confidence with which they surged ahead after that was truly a sight to behold. And the code became so easy to read. The rest of the app basically wrote itself. When it came to the UI they just had to assert that the correct data was being used in the correct place (they used MVVM and finally understood what testability of architecture means). Subsequent changes to the interaction model became one line fixes in some conditional and were delivered within hours with no need to QA.
This is the real power of TDD. When done right, it teases out the correct architecture. Most bad code is code militating against its architecture. This happens when the architecture is wrong, or when the architecture is not clear enough that devs don’t know when they’re fighting the architecture. The correct architecture makes everything downstream of it flow smoothly.
In addition, given a clearly understood architecture and a safety net of tests, the devs could undertake audacious refactors. Of course, mistakes were made in the initial iterations — for example one has to develop the instinct of writing tests of behaviour not of implementation — but these became apparent to the team quite easily and they improved their test intuition through these refactorings. And several rounds of iteration later you could see in the faces of the devs that they’d unlocked a new level of power in their personal programming mastery.
TDD may not be a silver bullet for writing software but I know of no other way to scale up a teams coding level and architectural sophistication than TDD.
I’ll write the tests later.
A - no you won’t.
B - it doesn’t work that way. Experienced TDDers are SMH when they hear this. It’s because they know how easy it is to write a test that tests nothing. If the test hasn’t failed before you wrote the code, you essentially have no guarantee that the test even does anything. And when you don’t have this guarantee, you will refactor like a champ, all the tests will pass and the application will fail in production.
Then you’ll come at me on Twitter saying TDD doesn’t work, stop pushing it, you know nothing svs. Trust me, just write the test first.
We’ll write the tests when the feature is stable
“How do we know users even want the feature?” asks the neophyte. “Why would we waste time writing tests if we don’t even know that the feature works?”.
Well, neophyte, I’d like to answer your question with a question - how will you safely change the feature after its launched based on user feedback if you don’t have tests? Will you add all the tests later? Really? You, who had so much resistance to writing tests for new code will somehow magically acquire the ability to cover an already written feature with tests? The reason you didn’t want to write the tests earlier was because you’re not fluent in writing tests. Making the job harder makes it less likely, not more, that you will have tests at some point in the future.
If you know enough about what’s being built to write code, you know enough to write a test for it.
How to start
Let’s get one thing out of the way first - there’s really no way to do TDD on an existing codebase of any size. There are a few things you could try though. You could try refactoring large classes into smaller classes and test-drive the development of the smaller classes. But be assured, you will not be feeling the benefits of TDD this way as only a small part of the system will be well covered by tests. It might also be possible to extract a part of the system into a separate service. This way you will atleast be able to isolate the benefits of TDD into a smaller system and gain full confidence over that part of it.
For people beginning TDD I’d suggest writing a library first. Libraries tend to have limited functionality and if you choose a use case without any I/O then you’ll have an easier time of it. Start with something a little bit complex — if it’s too simple a functionality then you won’t understand the power of TDD.
How did I start? I wrote a full web app without ever opening a browser. I had just tanked a company under horrendous technical debt and I vowed to myself that I would never put myself in that situation again. My explorations brought me to TDD and the next app I wrote I wrote every single line of code in response to a failing test, starting with the e2e tests, router tests, controller tests and unit tests for the models and the business logic.
Everything changed. Anyone who says that writing the test first and writing all the tests once the feature is stable will lead to the same outcome has likely never written a test and has definitely never done TDD. Everything I know about program architecture I learned during that intense period of study.
The next freelancer gig I got I wrote a cab aggregator system in a few weeks. It was stable out of the box and it never needed no QA. I could add new cab companies in a matter of hours. It handled a tonne of edge cases, had integrations with SMS and was quite a large and complex system. And it just worked. And I had an amazing time writing it.
Here’s an example of what the code looked like → https://blog.svs.io/extreme-decoupling-ftw/
If you’re a professional developer and are struggling to write bug free code or if you feel like you don’t have a deep understanding of architecture, you should take the time to learn TDD properly. It will transform your experience of programming and will level up your architecture skills.
If you’re an engineering manager, this becomes doubly important.
Ping me if you need any help getting started.