Learn the basics

You can’t skip this step: you must learn the basics first. I think the best way to get started is by attending an intensive training course, but that is of course my very much biased opinion as a TDD trainer. You can also pick up a copy of the book Test-Driven Development By Example, by Kent Beck, which is more or less regarded as the origin of modern TDD practice. And then there are numerous courses on YouTube, Pluralsight, Udemy, and other learning platforms, tailored to your specific programming language and level of experience.

Don’t just learn, practice!

TDD is easy. Uncle Bob summarized the entire process in just three simple rules:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

If you follow these rules, you will get the red-green-refactor cycle which is so typical of TDD.

flowchart LR R([Red]) --> G([Green]) --> RF([Refactor]) --> R style R stroke:#ccc,fill:#801918,color:#fff style G stroke:#ccc,fill:#293D21,color:#fff style RF stroke:#ccc,fill:#15173D,color:#fff

But there’s a difference between understanding the rules, and ingraining them in your thought process to the point where applying them becomes automatic. So, in the beginning, don’t overthink things and just practice. Start with basic exercises such as FizzBuzz or the Bowling Game kata. Then, find new exercises to do, and keep repeating your favorite ones until you can do them by heart. There’s a certain elegant rhythm to TDD, and the more you practice this rhythm, the easier it becomes to apply it in different scenarios.

Learn to use test doubles

There are two main ways you can use test doubles (i.e. mocks, fakes, stubs, etc.). The first one is to replace an expensive resource, such as a database or the network. You will often learn about this use of test doubles in courses and tutorials.

The second way is to replace your own code with test doubles. For example, if you have some API controller that calls into a service, you could write a test for your controller where you provide a mock service instead of the real one. This way, you can completely isolate the controller in your test.

flowchart LR subgraph Test fixture direction LR UT[Test class] --> Controller end Controller -.-> S[Mock Service]

Whether you consider this a beneficial and powerful technique or a definite anti-pattern depends on the style of TDD you are practicing. See the next section on the two styles of TDD.

Even if you’re not a fan of using mocks to replace your own code, you may still want to consider this technique. When you want to apply TDD in a big, existing, legacy code base, it can be really helpful to use mocks like this to isolate certain classes. It can be a great get-your-foot-in-the-door technique to start applying TDD in an existing project.

Learn how to deal with legacy code

Learning how to test and refactor legacy code is a very valuable skill to master. In simple exercises, TDD is relatively easy to do. Applying TDD in a fresh production codebase requires additional skill. And applying TDD in a legacy codebase can be outright challenging. You might face code that is hard to test, such as long methods, an over-use of static functions or variables, or tight dependencies between classes that are hard to break apart. You can learn how to deal with such situations by mastering only a handful of refactoring techniques. A good place to start would be this screencast.

Learn about the two main styles of TDD

There are two main styles or schools of TDD: the classicist style (AKA Chicago school) and the outside-in style (AKA London school). If you have read Kent Becks book, or practiced one of the original katas such as the Bowling Game kata, you have seen an example of the classicist style. If you have read books such as Growing Object-Oriented Software Guided By Tests by Steve Freeman and Nat Price, or have practiced the Bank Account kata, you have seen examples of the outside-in style.

I should hasten to say that these styles are not mutually exclusive. Although most TDD’ers have a preference towards one of these styles as their default, both styles have their merits and weaknesses. Knowing both these styles allows you to pick the right TDD approach for the right situation.

Typically, it is said that you need more experience and design skills to successfully use the outside-in style as compared to the classicist style. For me personally though, it wasn’t until I watched Sandro Mancuso’s excellent screencasts on the Bank Account kata and a refactoring exercise on legacy code until I began to see how I could start applying TDD in my day job. This highly depends on your background and the kind of software that you are working on, but these are excellent learning resources on the outside-in style nonetheless.

The architecture/design of your code (I’m using the terms loosely here) and the TDD process are closely related.

First of all, the quality of your design will dictate the ease with which you can write tests. In a loosely coupled, well designed system, tests are easy to write and maintain. In a tightly coupled code base, tests will be much harder to create and will typically involve very complex setups.

Secondly, the type of architecture will also influence the shape and style of your tests. In a typical 3-layered architecture, the tests you write tend to be somewhat different from the tests for a Clean Architecture-based system. Their setup will be different, and the unit or module that you are testing might have a different scope as well.

And then there is the concept of emergent design. In most examples of classicist TDD, a large part of your design will emerge in the Refactor phase of the TDD cycle, by cleaning up your code and removing duplication.

Personally, I have become a fan of domain-centric architectures (which go by names such as Clean Architecture, Onion Architecture, or Hexagonal Architecture/Ports-and-Adapters). In such an architecture, writing the bulk of your tests against the Use Case-layer/Primary Ports-layer seems to hit a sweet spot: tests are very expressive, easy to set up, and hardly tied to your implementation logic, which allows for a lot of freedom to refactor your production code.

For more information on this combination of TDD and architecture, I highly recommend this presentation by Ian Cooper.

Accept that learning takes time

Learning TDD is just like learning how to drive a car: you don’t really learn how to drive until after you have gotten your drivers license. Similarly, you only really learn TDD after having learnt the basics and after you have accumulated some experience applying it in real-world code bases.

The best way to accelerate that process? Practice! Practice the exercises, both classicist style and outside-in style. Repeat the code kata. Practice testing and refactoring legacy code. Practice applying it in your day job, even if that initially takes some extra time. Eventually, you’ll pick up more and more tricks along the way and you will notice that the practice starts to pay off: you write code faster, easier, and with less defects, and you’ll hardly ever need to use the debugger anymore.

I hope one of these tips resonates with you. Let me know in the comments. Now go and practice your kata!