Bruno Raljić

A Java Developer's Blog

Why I (don’t) use TDD

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.

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 the line 180. 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 (line 180). 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!


Note: After the blog migration, I noticed some pieces of code were missing, so I added it after. If I missed some, please let me know.

, ,

One thought on “Why I (don’t) use TDD

  • During software development in any normal situation, every normal person will encourage you to write Unit test, acc test and etc..
    But TDD is methodology which will give you, as software developer, confident that you know that your classes do what they should do. It is normal tha you get reported bug even you are using TDD. TDD will not realese you of bugs but it will reduce a number of reported bugs.
    The most of people first will create implementation (dedline comming soon i don’t have a time for writing Unit tests first, you must be insane …), but at end developer will create a unit tests, on so called working code.
    But problem with this Unit test is how they are writen. In most case this unit test are there because of code covarge not because of implementation. Developer do not want to be critisized by the team leader, product owner or the other team member.
    So in this two cases (with TDD and without TDD) the purpose of unit test is different. With TDD you realy test your implementation, because implementation is product of your tests, in second case you are just focused to check did you coverd all if satemen, every loop so that your class looks nice in Sonar
    Igor Madjeric recently posted… TDD || !TDDMy Profile

Leave a Reply

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

Show my latest blog post with this comment!