Why I (don’t) use TDD

Hello everyone. I want to bring back one of my old articles. Wrote it in 2012. Long time ago. I found it while searching something on web archive site. I'll copy the content as it is, my thoughts on this topic haven't changed so much. This republishing is me paying tribute to that article.

Test driven development is a very powerful thing. Although I don’t use it on every day basis, I’m a big admirer of the whole TDD concept. When I first heard of it, it made me laugh. It sounded impossible to me (especially for my poor skills). It was something like looking into the future. How the hell I can possibly know what and how will I implement something. Sounds familiar?

From my point of view, it’s not easy at all to cross the border and step into the TDD world. You have to think differently. And you probably won’t change your mind over night. Or maybe you will, like in my case when the TDD was the only way out of the problem.

Why I don’t use it?

Well, let me present you one more scenario you are, I’m sure, familiar with. New requirements are there and, of course, deadline is near. If you step back and take a look at the whole picture, you’ll see that TDD can save you in general a lot of time. But when the deadline is near we are looking only the small piece of that picture. We just want to finish something before our next deployment, write some test after the implementation and forget about it (until our next meeting with Mr Debugger).

Another thing, I still think you need to be very experienced developer in order to perform truly test driven development all the time.

Why I do use it?

As I wrote before, I didn’t “believe” in TDD until it provided me an exit from a very tricky situation with lots of conditions, loops and complicated logic. I started to write code in a usual way, finished few blocks of code, but soon it became so big I didn’t know where to change, what to change, and if I change something will it work as before. My brain was just over capacity. A colleague of mine told me I was ready for one “TDD session”. He started with really simple tests and forced me to write the rest. Of course, he helped me to finish it later, also with some refactoring. That was the point where I started to look differently at TDD.

I don’t always TDD… But when I do it, it’s because I’m working on something very very complicated, or it’s something I wan’t to be 300% sure it’s working as I want (some financial stuff for example). It doesn’t need to be complicated or serious all the time. I tend to do TDD when I feel I could easily make mistakes and spend more time in debug mode than in developing. Those are situations when I use TDD.

You are not allowed to write any production code unless it is to make a failing unit test pass.

-- Robert C. Martin (Uncle Bob)

Short example

I hope this simple Java example will be enough to present you a basics of TDD. Imagine you have a simple ajax call and as a result you want your Java object presented as JSON. Ok, I know, you already have many ways to convert the Java object to JSON, but for this purpose lets convert it manually. As you can see in example below, there are a lot of double quotes. In Java, you have to escape them if you want to have them in your string, so there is a possibility that you will forget to escape all the double quotes. That’s one of the reasons I found this example interesting for TDD.

// JSON example
{"menu": {
  "id": "file",
  "value": "File",
  "popup": {
    "menuitem": [
      {"value": "New", "onclick": "CreateNewDoc()"},
      {"value": "Open", "onclick": "OpenDoc()"},
      {"value": "Close", "onclick": "CloseDoc()"}
    ]
  }
}}

Now, let’s say you have list of User objects, and you want to create JSON from it, but you don’t want to include all user fields, just few of them, for example first and last name.

We will start from the beginning. First we will write a test for a case when the list of users is null. For this case we will throw a IllegalArgumentException. I will use this example to show you how to properly test an exceptions.

@Test(expected = IllegalArgumentException.class)
public void testExtractUserNullAsList() {
    // preparing test data
    List<User> users = null;

    try {
        // testing the method
        ModelMapper.extractUsers(users);
    } catch (IllegalArgumentException e) {
        assertEquals("user list is null", e.getMessage());
        throw e;
    }

After that, you’ll write an implementation to make your test pass. Notice the line138, which is telling that it is OK for this test to throw the exception. Another thing, I put the method into the try-catch block. You can make this test pass without putting the method into the try-catch block. The thing is, when you have more complicated tests where you expect some method calls, only way to verify all of them is to verify them in the catch block and then re-throw the exception. So, if you omit the try-catch block, your test will pass (as you expect the exception) but many of your mocks won’t be verified. Below is simple implementation that will make the first test pass.

public static String extractUsers(List<User> users) {
   if (users == null) {
       throw new IllegalArgumentException("user list is null");
   }

   return null;
}

Now, let’s make another test. This time, we will pass the empty list. We are taking care of the small things first. Later we will do the complicated things. That’s how you should write your tests. For now, we want the empty JSON object to be returned.

@Test
public void testExtractUsersEmptyList() {
    // preparing test data
    List<User> users = new ArrayList<User>();

    // testing the method
    String result = ModelMapper.extractUsers(users);

    // assertions
    assertEquals("{}", result);

}

After adding some code, our method now looks like this:

public static String extractUsers(List<User> users) {
    if (users == null) {
        throw new IllegalArgumentException("user list is null");
    }

    if (users.isEmpty()) {
        return "{}";
    }

    return null;
 }

The next thing, we want to write tests for case we have few users in that list. First, we will write a test, of course, where we will define what we want as a result of the method. That test will look something like this

@Test
 public void testExtractUsers() {
    // preparing test data
    User john = createDummyUser(1, "John", "Doe");
    User jane = createDummyUser(2, "Jane", "Doe");
    List<User> users = new ArrayList<User>();
    users.add(john);
    users.add(jane);

    // method to test
    String result = ModelMapper.extractUsers(users);

    // assertions
    assertEquals("{\"users\":[{\"id\":\"1\",\"name\":\"John Doe\"},{\"id\":\"2\",\"name\":\"Jane Doe\"}]}", result);

 }

Of course, if we run this test now, it will fail. Now, we’ll extend our implementation. After finishing the implementation and successfully escaping all of the double quotes, my method now looks like this:

public static String extractUsers(List<User> users) {
    if (users == null) {
        throw new IllegalArgumentException("user list is null");
    }

    if (users.isEmpty()) {
        return "{}";
    } else {
        StringBuilder sb = new StringBuilder();
        sb.append("{\"users\":[");
        for (User u : users) {
            sb.append("{\"id\":\"")
            .append(u.getId())
            .append("\",")
            .append("\"name\":\"")
            .append(u.getFirstName())
            .append(" ")
            .append(u.getLastName())
            .append("\"}");
    }

    sb.append("]}");

    return sb.toString();

    }
 }

But, after running test, I realize I missed something. Test failed because I didn’t separate users by comma. This is where tests are also helpful. If I didn’t covered this with tests, I could continue to develop without noticing that I’m missing this comma and (maybe) I could have long debug time in Javascript in order to find why I can’t parse the response properly.

tdd.jpg

Here’s my method after small modifications:

public static String extractUsers(List<User> users) {
    if (users == null) {
        throw new IllegalArgumentException("user list is null");
    }

    if (users.isEmpty()) {
        return "{}";
    } else {
        StringBuilder sb = new StringBuilder();
        sb.append("{\"users\":[");
        for (User u : users) {
        sb.append("{\"id\":\"")
        .append(u.getId())
        .append("\",")
        .append("\"name\":\"")
        .append(u.getFirstName())
        .append(" ")
        .append(u.getLastName())
        .append("\"}");

        if (users.indexOf(u) < users.size() - 1) {
            sb.append(",");
        }
    }
    sb.append("]}");

    return sb.toString();

    }
}

OK, now I can continue. But, let’s say I don’t like that indexOf(u) mumbo jumbo magic. It doesn’t look nice. I have an idea and with all these tests, it won’t be a problem to do the refactoring. After all, refactoring is one of the steps in the TDD.

Fail, pass, refactor.

One question for you. Imagine you have some complicated Javascript component that do a lot of work and hardly relies on your JSON response and you don’t have unit tests. Would you dare to do the refactoring if you have a deadline? Would you dare to do the refactoring at all? In this case, we have encountered with the double quotes escaping and adding commas. If we had few more things like that, every one of them will be a valid argument against the refactoring.

Let’s get back to the refactoring. In this example I’ll use Iterator because the hasNext() method will be perfect replacement in the if statement. Next step is to refactor until the tests are green again. After introducing Iterator, my method looks like this:

public static String extractUsers(List users) {
    if (users == null) {
        throw new IllegalArgumentException("user list is null");
    }

    if (users.isEmpty()) {
        return "{}";
    } else {
        StringBuilder sb = new StringBuilder();

        sb.append("{\"users\":[");

        Iterator<User> userIterator = users.iterator();

        while (userIterator.hasNext()) {
            User u = userIterator.next();
            sb.append("{\"id\":\"")
            .append(u.getId())
            .append("\",")
            .append("\"name\":\"")
            .append(u.getFirstName())
            .append(" ")
            .append(u.getLastName())
            .append("\"}");

            if (userIterator.hasNext()) {
                sb.append(",");
            }
        }

        sb.append("]}");

        return sb.toString();

    }
}

Epilogue

In this blog post I didn’t say anything new regarding the TDD concept. There are already a million blogs about it. It’s more like to say what I think about the TDD.

Example in the end was chosen because it’s not complicated to present it, and still can be used to explain few TDD things. We gone through writing test from very simple implementation to the final, let’s say, complicated implementation. We also see how can it help you to see where did you go wrong, and at the end we gone through the refactoring. It’s amazing when a programmer knows the application will work the same way after the refactoring, and the well written test will say – “Yes, you did the refactoring right”.

In the end, we all know TDD is good but still we don’t use it. From my point of view, it can be explained as:

Life is too short to remove USB safely!


Thanks for reading. As I wrote in the beginning this is the article from 2012. That means the code examples, skills and everything is also from 2012. The only thing I've changed is the referencing exact lines in the code examples. In the previous blog I had a highlighter that included those numbers and this one is without it.