TDD or no TDD? That’s the question.

Your project is going to be a big mess and every new functionality is hard to implement? You don’t remember or don’t know how this mass of code works? If you ask these questions sometimes you better take a look at this article.

I believe you’ve heard of the TDD. It’s a methodology that gives you a lot of advantages if used correctly. In this article, I’m going to share my experience with TDD, reasons to use TDD, examples, and more.

Content

  1. What is TDD and when to use it?
  2. Expected advantages and disadvantages
  3. Why should we use TDD? (Example of wrong tests, etc)
  4. Which troubles we can face in case of omitting TDD
  5. Difference between TDD, BDD, and XP
  6. Conclusion

1. What is TDD and when to use it?

TDD stands for Test Driven Development. That means you develop your application through the tests. Each module should be tested. But the most important thing here is that you should write test cases before your code implementation.

It looks like you create some expectations in a test case, you define how you would use your functionality and what is a result. It’s like a plan that allows you to create more flexible architecture.

Just an upfront example, you’d like to create simple calculation util class. You have a few common operations like adding, subtracting, etc. You’re expecting that result of subtraction between two positive numbers which are 5 and 3 is going to be 2. And you want to implement functional according to this knowledge. It sounds like a reason to create the first test case: do subtraction between value 5 and value 3; expected result is 2.

You don’t have any implementation at all, only the idea in your mind. And you need to create some kind of contract between expectations and an implementation. And this contract will be implemented in your tests.

Let’s see the next code:

@Test
public void testSubtraction() {
    int result = MathUtil.sub(5, 3);
  assertEquals(result, 2);
}

We have the expectation in this code. We think that a class is supposed to act like this. We don’t even have this class (or sub-method at least). If you run this test, it doesn’t work at all. You got a red broken test. You don’t have any implementation yet.

After creating this test you have to develop the first implementation according to the expectation.

public class MathUtil {
    public static int sub(int a, int b) {
        return a - b;
  }
}

And now your test passes.
What did we do? We created some test class with a simple test case to understand which behavior we expect. And after that we just implemented the logic.
Sure, it’s an extremely simple example. But the point is that we started from tests and realized how to implement the logic. Of course, in case of complex tasks and large systems your workflow would be a little different than in case of a MathUtil class. But the common idea about the “first test, then logic” is exposed.

Note: there are a few sources telling you that you should follow to this methodology using steps like creating a test which is not passing, create a stub or an upfront implementation to let the test pass, refactor, etc. Yes, it’s absolutely correct. In that case we just have a simple implementation to show the whole idea of TDD. In the following chapters I will touch this point too.

The reasons to use TDD or other approaches.
It’s time to talk about a background, why should we use tests in a development. I think you faced a lot of cases when a code quality was so bad. It was some kind of a headache attempting to clarify the code structure and the whole mechanism. And also it was a big pain in the ass when you tried to change a logic or add something completely new. I’m not even talking about refactoring in these projects. If there were no tests, that could assure you that nothing is broken when you change some code. Even if you had tests, they were just another problem with bad performance, they didn’t test correctly, they used a reflection or another way to invoke an encapsulated logic. These tests also were created because of a coverage metrics, but not to help developers and project at all.

Sounds familiar?

Writing tests correctly can give you at least a good marker of quality and reliability. When you can be assured that tests pass correctly, you can be assured that you get actual check of new code changes. You just can avoid possibilities to get any mistakes in a project.

TDD is a methodology that describes how we can develop using tests more effectively. There are a lot of other methodologies like BDD, eXtereme Programming (XP), and so on. We’ll talk about that later.


2. Expected advantages and disadvantages.

So, which advantages we can expect using TDD approach?
* reduced development time
* completed code or fully clean code
* no bugs

I think there were the most common thoughts about TDD advantages. Let’s discuss them.

Reduced development time.
You can think that using TDD approach may increase your development speed right away. No, it doesn’t. You can’t get increased speed right after you start using TDD because you need to understand this concept, but with an experience you will write tests even faster. It’s not the point. The point is that just after using TDD you develop a bit slower at the start because you need to write more code. If you write some method you write the test method either.
But why it does?
When you develop using TDD you create more flexible code and architecture that means you can make changes or new features faster. It means if you have already some expertise in TDD and can do steps in TDD quickly, it can be faster because you spend less time thinking about design and architecture, but also you get a flexible code. The point is that you can avoid any deceleration stuff of TDD according to your experience with this methodology.
If you don’t use tests at all or write tests in a creepy way you can save time when you start a project. You will develop faster (but it’s not exactly the truth). But after a few weeks or months, your development speed will be decreased. New features will be harder and harder to integrate. Any refactoring will be a mess. Here comes a TDD approach. Now you can develop stably at least. You even can get some acceleration comparing to an approach without tests ( or with bad tests to get some coverage ).

Take a look at the diagram below. Although it’s abstract and values are approximated, you can see how speed value changes during TDD comparing to the development without tests. You can notice that after start you can’t feel good acceleration using TDD. But a few iterations later you can see perspectives of TDD approach and perspectives of the approach without tests.

After a start yellow chart is faster than green, because you don’t need to write tests and to be distracted by ensuring tests pass. But after the very first new feature in the next sprint the speed of the both processes decreases. But since you don’t have any tests in the second case we see that speed fell more significantly. And you are getting more confused by the next feature over and over again until your development is stucked (it may not let your development be fully stopped, but it can be dramatically slowed down). In TDD you get some decrease in speed but always restore previous or similar speed.

Perfect code or fully clean code
No, you won’t get completed code or clean code only because you use TDD. Test Driven Development allows us to use an opportunity to increase the code quality. But using this opportunity is on you. You still have to follow the requirements for the clean code, performance. And even more, you still have to implement all these requirements. TDD allows you to look at your code and architecture from a few different perspectives. According to this you can make decisions about design, clean code tricks, and so on. But again, it doesn’t enforce you to do that and it doesn’t work on its own.

No bugs
It won’t be right to say “there are no bugs because we use TDD”. Yes, you decrease the number of bugs as much as you can when you use TDD. But it doesn’t guarantee that you won’t get any. Bugs can be differentб depending on your business logic, technologies stack, and so on. We are able to decrease bugs frequency by using TDD. But bugs still can appear.

Number of bugs is less because you can prevent most of them during TDD steps and when you review the code.

Conclusion
We can expect that our work process will be simplified because everything we create is already tested, and you can be assured that it works correctly. There is no magic to expect. It won’t give you an extra performance. But if you understand the concept you develop confidently. It’s a really good practice when you can rely on your code and tests. And, of course, all future changes won’t be a surprise. You won’t suffer from the regression issues in the code you wrote before. It’s just not possible because of a general idea of TDD.

3. Why should we use TDD? (Examples)

I have an experience in a few projects where unit tests were written for coverage only. That was a really big mistake. I suggest you to avoid projects where code quality isn’t an important thing. I like exploring code I work with. And I faced strange decisions, faulty implementations caused by not following TDD or not creating good test cases.

I would like to share one of these issues here.
So, let’s imagine that you have to implement some util service which works with some incoming list of car sales journal records. You don’t know how many car brands will be in the list. But brands can be repeated. You have some object that looks like:

package space.eignatik.DTO;

import java.util.Map;

public class SalesItem {
      private String code;
    //In properties can be stored a few 
    //fields like brands and a few more information
    private Map<String, Object> properties;

    public String getCode() {
        return this.code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public Object getAttribute(String key) {
        return properties.get(key);
    }
}

The structure is simple. It represents a record from the car sales journal. You get List<SalesItem> that contains a large number of items. And your task is to create a list of car brands. This is a converter from a list of sales records to a distinct list of brands. When I was looking at implementation it was like the following code:

package space.eignatik;

import space.eignatik.DTO.CarDTO;
import space.eignatik.DTO.CarDTOItem;
import space.eignatik.DTO.DictionaryItem;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class CarBrandsService {

    /**
     * Returns all input brands with distinct filter.
     * @param items input values List
     * @return CarDTO with filtered list on the board
     */
    public CarDTO distinctBrand(Iterable<DictionaryItem> items) {
        if (items == null) {
            return null;
        }

        List<CarDTOItem> carDictionaryDtoList = new ArrayList<>();
        String previousBrand = "";
        String currentBrand = "";

        for (DictionaryItem dictionaryItem : items) {
            currentBrand = (String) dictionaryItem.getAttribute("brand");
            if ("".equals(previousBrand) || !previousBrand.equals(currentBrand)) {
                previousBrand = currentBrand;
            } else if (previousBrand.equals(currentBrand)) {
                continue;
            }
            CarDTOItem carDTOItem = new CarDTOItem();
            carDTOItem.setCode((String) dictionaryItem.getCode());
            carDTOItem.setBrand(currentBrand);
            carDictionaryDtoList.add(carDTOItem);
        }
        CarDTO carDTO = new CarDTO();
        carDTO.setCars(carDictionaryDtoList);

        return carDTO;
    }
}

And also was a test for this method. It was just creating stubbed data and checking that size of output list would be 10. I don’t know where the previous developer got this magic number – 10. There is also a test that checks a case when “null” is returned (that’s just assertNull(distinctBrands(null)), we won’t see on this test). But the previous test is presented here:

@Test
public void testIfDistinctBrandWorks() {
    List<DictionaryItem> items = getItems();
    CarDTO dto = service.distinctBrand(items);
    assertEquals(dto.getCars().size(), 10);
}

The point is that it works. It’s a green. And it gives us 100% coverage. Look at the code. Each line of code is covered. Green markers on the left of this picture represent lines that are covered by tests. Lines with no coverage would be marked red. The manager is surely glad to see that.

Pretty nice picture, isn’t?
But there is only one problem here. This code cannot be trusted.
I believe the author of the code tried to only check the case with two identical car brands that stand shoulder-to-shoulder. What about other cases? The main goal of implementing this method was a coverage. This method has been added much later than source class. It means that author developed a test based on implementation, but not on busines logic. And it may cause the test to be false possitive: provide coverage without fully checking the logic.

Take a look at these pictures. These are two lists: input and output. Pay attention to the two red highlighted items. Is it like distinct should work? I don’t think so.
This is a consequence when you don’t use TDD or don’t test your code in a proper way.

Let’s add additional lines (4 and 6) according to requirements. They will count unique records of brands into Set to get the expected size. And test works no more. If a developer made this test before implementation, he would not miss that. When you start thinking from the point of what you expect, you develop that correctly. But once you tried to create a test for coverage for existing code, then your code can’t be reliable even if tests are passing.

    @Test
    public void testIfDistinctBrandWorks() {
        List<DictionaryItem> items = getItems();
        Set<String> brands = new HashSet<>();

        items.forEach((item) -> brands.add((String) item.getAttribute("brand")));

        CarDTO dto = service.distinctBrand(items);
        assertEquals(dto.getCars().size(), brands.size());
    }

Using TDD helps you avoid incorrect implementations and misunderstandings. You can rely on the code you write and make changes safely. That’s really important to keep the development rate and not to miss the deadlines.

4. Which trouble can we get by omitting the TDD?

If you want to avoid troubles I mentioned before you better be friends with TDD. Just image if you found this issue on the production when your customer already lost some profit because of wrong reports based on these data? It’s better to find mistake in time or don’t do mistakes. And TDD can help with both. Also coverage will be higher. Every line I wrote using TDD is covered, this is a feature of TDD approach. You automatically get 100% coverage if you follow TDD correctly. Just make sure to work on the test cases thoroughly.

Somebody can say “That’s not the point, you don’t know if customer loses the money or he doesn’t . It can’t be related to TDD or not”. This is not precise. First of all, it’s not about TDD at all, it’s about code quality and product defects. TDD is just a tool. If you use another tool, why not? The point is that customer will lose more money if you have been mistaken in any implementation.
Customer relies on his product. Remember, those tests were successfully passing before, you were able to deploy a wrong implementation on the production server. Once you did it and customer got some unpredictable behavior he may lose a lots of cash. Maybe you have been mistaken in authorization service, and nobody could log in to open the new account or deposit money. How much money will customer lose? How much loyalty from the customer may you lose?

Be professional, rest assured that your code can be trusted and relied on.

A collegue from one of the non-TDD project once asked me:
“Where can an error be here?”
“Everywhere”, I answered, “How can you trust this code? It isn’t even covered by unit tests.”
“But I reviewed this code and I know how it works.”
“There are a few very large classes. How can you be sure that there is no mistake if you can’t even tell what the logic is implemented there and no test can assure that.”
After a few deep analysis sessions we found five mistakes that could be noticed only with proper tests. And could be avoided.

5. Difference between TDD, BDD, XP.

It’s not correct to say that these things are alternatives to each other, though they are often reffered to as ones. TDD is not just about how to write tests, it’s about a good design and writing extendable code. Extreme programming (XP), on the other hand, is the whole framework (in agile point of view) which can contain TDD as a part of it.
In my humble opinion, XP is a great thing at all. It consists of a lot of features like pair programming, continuous code review, etc. It happens to be very helpful.

BDD might be interpreted as an extension of TDD. While you write unit tests to make a more flexible design using TDD, in BDD you can operate with a little bit another abstraction level. You can find out that business requirements, software quality, and implementation are connected more tightly when using BDD. In both cases, tests should be written first.
Let’s say that you have some requirements. And these requirements can be interpreted into test cases like this:

Given user can be authenticated as a common user into their account
And this user’s balance is $300
Then when user transfers $100 to another account,
Current account has $200
And new account has $100

Sometimes when we use TDD we unknowingly write BDD tests. I think we need to keep adhere with one methodology at a time. Class with component tests doesn’t need to contain BDD tests, and vice versa. Simple unit tests check results of simple operation or util operation. BDD tests should test business functionality. I used to separate just unit tests and BDD stuff.

I stick to the idea of using TDD in development. Of course, I’m also a proponent of XP (at least many of its aspects) and BDD. But in my point of view, TDD is the most important methodology in code and design quality.

6. Conclusion

Learn more about TDD steps in the book “TDD by example” by Kent Beck. This book covers a lot of examples step by step. If you really want to dive deeper into examples, then this book is for you.
My goal was sharing my experience in TDD. Be sure to check other books and articles about TDD to get even more from this.

A few helpful links:

I also suggest you have a look on these books:
Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin Series): Robert C. Martin: 9780134494166: Amazon.com: Books
The Clean Coder: A Code of Conduct for Professional Programmers (Robert C. Martin Series) 1, Robert C. Martin, eBook – Amazon.com

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Leave a Reply

Your email address will not be published. Required fields are marked *