What is TDD? [Roadmap to Implement TDD in Your Organization]

Shares

What is TDD? [Roadmap to Implement TDD in Your Organization]

Hardik Shah
in Quality Assurance
- 18 minutes

Programming is demanding. One can get good at it after consistent efforts for months and years. At best, development team mistakes lead them to write the code that won’t compile. At worst, these mistakes lead to bugs that does the most damage.

Wouldn’t it be cool if you have a technique that can virtually eliminate the need for debugging, and alerts you to programming mistakes after you’ve made them?

There is such a tool or rather, a technique. It’s test-driven development, and it actually delivers these results.

What is Test-driven development(TDD)?

Test-driven development is an iterative development process. In TDD, developers write a test before they write just enough production code to fulfill that test and the subsequent refactoring. Developers use the specifications and first write test describing how the code should behave. It is a rapid cycle of testing, coding, and refactoring.

The key ingredient for being effective with test-driven development is understanding what it truly is. I find that there are a lot of misconceptions around how to do TDD properly. TDD is one of the practices that if you do it wrong, you often pay a hefty price.

Rather than thinking about doing TDD as a way of developing code, you can think of doing TDD as a way of specifying behaviors in the system. It leads you to create very different kinds of tests that tend to be more resilient to change in the future, because you’re verifying behaviors rather than testing pieces of code. Let’s see how it’s done in three steps: Red-Green-Refactor.

TDD means letting your tests drive your development (and your design). You can do that with unit tests, functional tests and acceptance tests. Before you understand the TDD process it’s important to know about Unit testing. Here’s my previous blog to know more Unit testing: How Unit Testing Improves Code Quality in TDD.

TDD process is definitely tedious and following it is equally difficult. But I have tried to summarize the process in the following flowchart. In this blog, we would be discussing each part of the process starting with Red-Green-Refactor.

what is tdd

Step 1: Create a test and make it fail (Red).

  • Write the unit test for the function you’re going to implement, the unit test should be short and focus on a single behavior of a function.
  • Write just enough code so that it compiles.
  • Run the test. It should fail. By writing the failing test you ensure that your test is calling the correct code and that the code is not working by accident. This is a meaningful failure, and you expect it to fail.

Step 2: Make the test pass by any means necessary (Green)

  • Write the minimal code to make the test pass.
  • You are done for the particular code if the test passes as expected. You do not have to write more code with uncertainty.
  • Run your test again, and watch the test pass. This will result in a green progress bar.

Step 3: Refactor

Once your test passes, you can now refactor without worrying about breaking anything. Review the code and look for possible improvements to keep the code clean.

  • Remove duplication caused by the addition of the new functionality.
  • Make design changes to improve the overall solution.
  • After each refactor, rerun all the tests to ensure that they all still pass.

What follows are the patterns for TDD. When you go through each step of TDD (red-green-refactor), you will face small challenges like which test should you pick or how to refactor? Following are the patterns for all three steps which will help you complete your cycle effectively.

Red Bar Patterns

One step test

Which test should you pick next from the list? Pick a test that will teach you something and that you are confident you can implement.

There are two approaches: top-down and bottom-up. In top-down approach you begin with a test that represents a simple case of the entire functionality. In bottom-up approach, you start with small pieces and aggregate them larger and larger.

Neither top-down nor bottom-up really describes the process helpfully. A vertical metaphor is a simplistic visualization of how programs change over time. In TDD, while picking up the first test, know-to-unknown vertical can be useful.

Starter test

Which test should you start with? For that, you need to start by testing variants of an operation that doesn’t do anything.

When you try to create a test that covers too much like “how do I test that this program identifies faces?” it can be difficult to accomplish. So create small starter tests such as create a class/method/etc where you give it a picture or nothing at all and it returns data back.

Explanation Test

Organizational adoption is the key to TDD. To spread the word of TDD to others without forcing them to convert, try to talk in tests. For example – ‘The interaction of X and Y, leads to Z whereas if you use A and B the result would be C’.

Learning Test

It suggests that the best way to learn about a new facility in an externally produced package of software is by writing tests. If you want to use a new method, class, or API, first write tests to learn how it works and ensure it works as you expect. Learning tests provide a convenient mechanism for exploring an API in an isolated, incremental and reproducible way.

Regression Test

You might wonder how regression testing is a Red Bar pattern. Because these tests you would have written while coding originally. Now at the smallest level these are a way to improve your testing by realizing that you may have to refactor the code before isolating the defect.

At the same time you will gain value by testing at the application level. Testing the whole application gives an understanding from the user perspective whether the application functions as expected.

Green Bar Patterns:

Fake It

What is your first implementation once you have a broken test? Even if the function or method returns a hardcoded number, just fake it. This will help turn the test green, but it also helps control the scope of the project. Maybe it returns a constant, but maybe that’s all its supposed to return.

Triangulate

You write 2 or more tests so the code doesn’t just return constants like the Fake It pattern. It helps create a level of abstraction for the code. This technique is really great when you’re trying to find the right abstraction/algorithm to fix the problem.

Obvious Implementation

Sometimes, you need to Fake it or use Triangulate to solve a difficult implementation. But sometimes, you just know how to solve it. In that case, just solve it. The other steps are there when you don’t know how to solve it and you need to take small steps to break down the problem and solve it that way. Obvious Implementation is second gear; be prepared to downshift if you get stuck.

Refactoring patterns:

Extract Method

Of all refactorings, Extract Method is the most common refactoring technique. Extract method is useful when common blocks of code has been used in multiple places, or when there’s a possibility to divide the code into pieces to make it easy to understand.

With this technique you separate a common block of code into a new method (or function) and replace the old code with a call to the method. The main benefits is that you can often reuse the pieces and avoid duplicating logic in other areas of the system. Here’s an example of Python:

Inline method

Inline method is helpful when you have a group of methods that seem badly factored. Sometimes using a method’s body in your code turns out to be more obvious than using the method itself.

It is opposite of the Extract Method refactoring. It lets you move the method’s body into the body of its callers and remove the method declaration, if needed. You can inline them all into one big method, and then re-extract the methods.

Another reason to use Inline Method is that sometimes you do come across a method in which the body is as clear as the name. When this happens, you should then get rid of the method. Let’s take an example:

Extract Interface

Extract method is used when multiple clients are using the same part of a class interface. Another case is when the part of the interface in two classes is the same.

Using extract method, you can move the identical portion to its own interface. You create an interface for a class with declarations for all of the methods that you want to use in some context. When you’ve done that, you can implement the interface to sense or separate, passing a fake object into the class you want to test.

                                 

Move Method

This technique is used when the method is used by many features of another class than the class on which it is defined.

In move method technique, you need to create a new method in the class that uses the method the most, then move code from the old method to there. After that, Either turn the old
method into a reference to the new method in the other class or else remove it altogether.

Method Object

Long methods are very tough to work with in many applications. Many times the local variables are so intertwined with the long methods that you cannot apply Extract Method.

Using Method Object technique, you can transform the method into a separate class so that the local variables become fields of the class. Then you can split the method into several methods within the same class.

Add Parameter

This technique is used when a method does not have enough data to perform certain actions.

You need to make changes to the method and these changes require adding information or data that was previously not available to the method. Simply, Add Parameter technique is about creating a new parameter to pass the data.

Dependency Injection

When writing unit tests you just need to test one method of your application, if your method relies on another class/variable there should be a way you can inject this into the method. This is where dependency injection in your code comes in handy, it will allow you to inject objects into your classes to change the output of the class.

Dependency Injection (DI) flows naturally while working with TDD approach. It’s difficult not to use DI in TDD.

Let’s take one example. For example, we have a login class that will connect to a database.

class login
{
     private $db = false;

     public function __construct()
     {
          $this->db = new Database();
     }

     public function loginUser( $user, $password )
     {
           $this->db->checkLogin( $user, $password );
     }
}

This login class has a dependency of the class Database in the constructor, which means that we can’t unit test this correctly. If we want to unit test then the database class has to be developed and tested. If the database class is broken and we try to unit test the loginUser() method the test will always fail. Thus we won’t know the cause of failure – Is it the database class or loginUser() method.

If the database class is developed, tested and data is in the database then we can use this for the loginUser() function. But now our tests are dependent on data being correct in the database. If we pass in a username and password it must be in the database for our test to pass. Our code could be correct but if the data isn’t there then our unit tests will fail. This isn’t correct use of unit tests and is more suited to be an integration test.

To fix this problem we can use dependency injection to pass in a database connector which will set the database class variable. There are 2 ways we can inject a variable into a class, it can either be in the constructor of the class or by using a setter method. I tend to use constructor for all required dependencies and use the setter method if there is a default value for the class variable.

class login
{
    private $db = false;
    public function __construct( $db )
     {
          $this->db = $db;
     }

     public function loginUser( $user, $password )
     {
          $this->db->checkLogin( $user, $password );
     }
}

Now this class isn’t dependant on a certain database class we can pass in the database class by using the parameter on the login class constructor.

We can unit test this loginUser() method by first setting the $this->db class variable. We don’t want to rely on a real database as the data can change so we can either create a test harness database class or you can mock the database class.

A test harness class will allow you to create your database class and hardcode any data that you need. In the example above we can create a method checkLogin(), in our test harness we can then hardcode a successful login username and password to make the loginUser() method pass. Or you can use a PHP mocking framework to mock a class/method/return value.

A unit test should test a single codepath through a single method. When the execution of a method passes outside of that method, into another object, and back again, you have a dependency.

When you develop software, you use the variety of classes and components. Out of all the classes, many classes depend on other classes/components to run the functionality. For example, Your user interface is dependent on your business domain classes, and those same business domain classes are themselves be dependent on things like external data stores (databases, file systems), web services or other external resources and systems.

While doing unit testing it’s essential to test only one thing and in isolation. When you test classes with dependencies you are doing integration testing. Also, when you test with external dependencies the test becomes unpredictable.

Speed is another issue. I want my tests to be fast. Code that has to interact with an external resource like a database or a web service will run slower due to the latency in the communication with these external resources.

All the above concerns lead us to Mocking. Let’s see how it is useful in writing unit tests in TDD.

Mocking

Mock objects are a popular tool for isolating classes for unit testing. When your tests depends on another method to return a value, you can create objects which will act as a certain class and mock the method and make it return any value you want.

The mock object checks that it is called correctly and provides a pre-scripted response. Mock objects make your tests fast as it avoids time-consuming communication to a database, network socket, or other outside entity.

When to use mocking?

Before we decide when to use mocks, let’s first see when mocking should be avoided.

The following sums up Uncle Bob’s post on When to Mock:

No mocks

  • Slow test suite – database, API, … are slow and asynchronous
  • Low test coverage – can’t test all states and edge cases
  • Fragile tests – slow, down, changing dependencies

Too many mocks

  • Slow mocks – mocks that depend on reflection can be slow
  • Fragile tests – mocks that return other mocks are complicated and tightly coupled
  • Too many protocols – a mock provides a simpler implementation of a protocol interface

It’s about finding the right balance. The answer is somewhere in between these two extremes.

But where?

Mock across architecturally significant boundaries, but not within those boundaries. For example, mock out the database, web server, and any external service.

Best practices of TDD

Pair Programming

Pair programming is a practice in which two programmers (called the driver and navigator) work together at one workstation, collaborating on the same development tasks (e.g. design, test, code). The driver, is typing at the computer or writing down a design. The navigator observes the work of the driver, reviews the code, proposes test cases and considers the implementations strategic implications. In case of solo programming all activities are performed by one programmer.

Often, Managers worry that using paired programming will double their programming costs. At first glance, it is understandable why they might think that- after all- two developers are being asked to work together on the same task.

Michael D. Hill counters above logic with one simple argument, It’s true that two developers together don’t type as fast as two developers separately, but the bottleneck is the thinking [not the typing], and pairing and TDDing both improve it.

When Pair programming is effective?

Pair programming is more effective in complex programming tasks. Pairing is about improving design and minimizing mistakes. In complex programming tasks, single developer would be significantly slowed down and have a higher probability of making mistakes. Having another developer on hand reduces these problems.

Pair programming is extremely useful for onboarding new developer. A more experienced developer can pair with a trainee to quickly and effectively share knowledge of the project and general coding skills.

Putting pair programming into practice

When doing TDD the driver writes exactly one test until the code can be compiled, but the test is red. The driver switches the role to the navigator. Now the driver, who was the navigator before, needs to make the test pass by writing the simplest code possible. After that, the driver writes a new test until the code can compile, but the test is on red. Now the driver and the navigator switch roles again. In this way, considering that you have a similar average time to write all the tests, both the driver and the navigator have the chance to write tests and to write implementation code.

Testing Legacy code in TDD

One of the common questions people ask while getting started with TDD is: “I find starting with TDD on an existing project hard. How do you get old legacy code testable?”.

Unfortunately, one of the most common answers to that question is: “You just have to begin and take small steps.” And while the answer has truth in it, it’s also a much too simple answer that doesn’t really help you to start. What would be the detailed answer for it? Let’s see.

When the code you exercise produce a different result than the result you expect, the test fails. But automatic tests also show you another kind of mistakes: design flaws. If you want to test some code but you can’t write a test for it because it’s impossible to isolate the part of the code you want to test (due to dependencies, couplings etc.), that’s due to a design flaw. Therefore, what makes TDD hard in an existing project is not TDD itself, but the Technical Debt you have to consider.

You need to know design principles like SOLID, DRY, YAGNI and design patterns to know where you need to go with your design to have low-coupled and well-structured code with that’s easy to test.

To have high quality code, you don’t only need to know common design patterns and principles, but you also need to know which problem they, and why and how they work. Then you need to recognize code smells in your code and know if and how a pattern can help you to get higher quality code that is testable.

Adapt Parameter: When you can’t extract the interface, and the parameter you want to test is difficult to fake.

Break Out Method Object: If your method is large or does use instance data and methods.

Encapsulate Global References: To test code that has problematic dependencies on globals

Expose Static Method: If you have a method that doesn’t use instance data or methods, you can turn it into a static method.

Extract and Override Call: If there’s a dependency on global variables and static method call.

Extract and Override Getter: If dependencies hidden in constructors can be tackled

Extract Interface: If you have trouble creating a class that wraps the original class in a test harness.

Introduce Instance delegator: When static methods are hard to fake in the absence of polymorphic call seam

Parameterize Constructor: When the dependency is hidden in the Constructor.

Pull Up Feature: When you have to work with a cluster of methods on a class, and the dependencies that keep you from instantiating the class are unrelated to the cluster.

Replace Function with Function Pointer: To break dependencies in procedural languages

Subclass and Override Method: When you can’t extract a dependency outside of a class under test.

When should you delete tests?

More tests are better, but if two tests are redundant with respect to each other, should you keep them both around? That depends on two criteria.

  1. The first criterion for your tests is confidence. Never delete a test if it impacts on the behavior of the system.
  2. The second criterion is communication. If you have two tests that exercise the same path through the code, but they speak to different scenarios for a reader, leave them alone.

That said, if you have two tests that are redundant with respect to confidence and communication, delete the least useful of the two.

Conclusion

Using TDD properly, has inherent benefits such as less time in debugging, high test coverage, higher confidence in your codebase as it’s well-tested etc. Also, you can find mistakes very quickly and have little difficulty fixing them.

Although TDD is a very valuable tool, it does have a two-or-three month learning curve. It’s easy to apply to small problems, but translating that experience to larger systems takes time. Legacy code testing, proper unit test isolation and refactoring are particularly difficult to master. On the other hand, the sooner you start using TDD, the sooner you’ll figure it out, so don’t let these challenges stop you.

Hardik Shah

Working from last 8 years into consumer and enterprise mobility, Hardik leads large scale mobility programs covering platforms, solutions, governance, standardization and best practices.

what is tdd

Subscribe to our TDD updates

Like what you're reading? Subscribe to get our latest updates on Test-driven development! No spam, only high-end resources directly into your inbox! 

You have Successfully Subscribed!