My Journey to TDD

Vinny Joseph
priceline labs
Published in
4 min readDec 19, 2019
A laptop with some code showing — we hope these are tests!

Despite having heard a lot about Test Driven Development (TDD) during my career, spanning over a decade, I had never practiced TDD until a few months ago. As a personal challenge, I decided to experiment with TDD as one of my quarterly goals. Hence, in addition to the usual weight loss and self-improvement goals, I set a new goal to do TDD this year.

Historically, I always found reasons to evade TDD.

Ways I have avoided TDD

TDD is almost impossible on legacy code.

It is really hard to do TDD on legacy code due to the old nature of the code base, the lack of modularization, heavy usage of global variables, hardwired dependencies and lack of existing unit tests.

TDD is time-consuming.

TDD requires thought and analysis prior to writing code, and is thus time-consuming.

TDD means changing habits.

It became a habit to write code by thinking while you type. It is always hard to change a habit or to start a new one.

The product owners won’t understand the value of TDD.

Are the product owners willing to compromise on the delivery time in order to add more unit tests?

Peer pressure.

What will my teammates think when I suddenly start to behave abnormally, i.e. doing TDD?

TDD is not practical.

How can I consider TDD practical as it adds time to deliver high priority tasks and delays features, which in turn slows revenue growth?

How I approached TDD

Although I had many reasons to feel discouraged, I decided to try TDD on my next task. I had to add a bunch of validation rules to some legacy code and recognized that it was a good opportunity to try my first-ever TDD, which went as follows:

  1. Using the single responsibility principle and abstraction, I isolated the new changes to their own class. This way the changes to the legacy class became minimal. I ended up having a new class with one simple boolean, no argument function, which always returns false.
  2. I wrote a unit test for this new function, which failed as expected.
  3. I began adding code to the new function, with the goal of making the test pass. As I added more code, I thought through how to make the code testable. This resulted in loosely coupled code with dependency injections.
  4. Once my initial test passed, I added more unit tests to cover the additional test cases, adding code to fix bugs as they arose. And whenever I made code changes, I reran all my tests to ensure the changes didn’t cause side effects.
  5. After my code was complete and all tests were passing, I started to refactor things. I reran the tests each time I made changes. Since I had great coverage on my code, I was able to refactor my code with confidence.

The end result surprised me. I had written some of my cleanest, most readable code to date. The simplicity of the code and its test coverage also surprised my teammates. Since then, I have used TDD many times, consistently producing code that I have great confidence in.

Here are some tips to getting started with TDD:

Start Simple

Always start simple. TDD differs substantially from the traditional development process. You will stumble on a lot of questions as you start to think about the code you are about to write. This is normal and it is one of the benefits of TDD. It makes us think about the design and dependencies earlier in the process. However, don’t let this confuse you. Start simple; write one failing test and work on making it pass.

Try Top-Down

It is much easier to try a top-down approach when doing TDD as opposed to a bottom-up approach. This means starting with the very high-level details and dependencies that we already know. Don’t worry about the minutiae of the code or the layout at the beginning. Once we have the tests in place, the code can be refactored.

Think First

Think before you type! This forces you to think about the code before getting started. While this introduces a delay in initial development cycle, the results will save time in the long run.

Continuous Refactoring

Once you have the initial test passing, the next stage is refactoring. This is where we can think more about the design principles and patterns, then apply them. There is always some uncertainty involved when it comes to refactoring code. However, with full test coverage, this becomes much easier and less concerning. Tests will ideally catch any bugs caused by refactoring. Always run the tests after each change to identify side effects right away.

Treat Unit Tests as Documentation

Consider the unit tests to be documentation. Ensure proper naming and descriptions for the tests. Clearly specify what we are testing and what is the expectation.

Conclusion

Changing habits always presents a challenge. The traditional way of coding became so habitual to most of us. I’ve started doing a lot more TDD since I first began practicing it, but I still appreciate the difficulty of taking the first step. I continue to find myself tempted to start coding in the traditional way, thinking while I type. But after gaining some momentum, I found myself writing clean and elegant code with excellent test coverage. If you have yet to try TDD, give it a shot. It’s worth your time!

Photo by Christopher Gower on Unsplash

--

--

Vinny Joseph
priceline labs

Full Stack Engineer @priceline. Java, Javascript and more.