Why Unit Tests Will Save You a Lot of Time

When you as a developer hear that you should write unit tests, it sounds like more effort at first. The code you have written works, as you have already seen by trying it.

However, the world around it is constantly changing. And how can you be sure that the code still works despite changes from the outside?

Date Format?

Let's assume that in our program there is a function to parse strings as dates.

/**
 * @param input a date with format "dd.MM.yy HH:mm"
 */
public static Date parseDate(String input) throws ParseException {
    return DateFormat.getInstance().parse(input);
}

We use a class from the JDK here and even describe in the Javadoc which format the input parameter should have. Once we have tried this, what else can go wrong? Surely this will work forever.

Even after updating from Java 8 to Java 11, everyone would assume that the function still works as described, right? After all, we are only using a class from the JDK.

Let's do a test:

@Test
void parseDate_should_parse_String_to_Date() throws ParseException {
    Date parsed = parseDate("14.02.09 00:31");
    assertThat(parsed).hasYear(2009).hasMonth(2).hasDayOfMonth(14).hasHourOfDay(0).hasMinute(31);
}

With Java 8, the test is successful. Everyone would probably expect the same with Java 11. However, the result is:

java.text.ParseException: Unparseable date: "14.02.09 00:31"

    at java.base/java.text.DateFormat.parse(DateFormat.java:395)
    at org.example.Dummy.parseDate(Dummy.java:31)
    at org.example.DummyTest.parseDate_should_parse_String_to_Date(DummyTest.java:36)

With Java 11 the default pattern of DateFormat was changed from "dd.MM.yy HH:mm" to "dd.MM.yy, HH:mm". Now as a developer you have to make a choice: Adapt the implementation of parseDate so that the interface (see Javadoc) is fulfilled again? Or adapt the interface (Javadoc) and all callers to the new behavior?

Currency Format?!?

A second example inspired by the update from Java 8 to Java 11. Let's assume that in our program there is a function to format numbers as currency. For simplicity, let's again take a class from the JDK.

/**
 * Formats 13.37 als "13,37 €"
 */
public static String formatCurrency(double input) {
    return NumberFormat.getCurrencyInstance(Locale.GERMANY).format(input);
}

Then let's do a test for it:

@Test
public void formatCurrency_should_format_double_to_String() {
    String formatted = formatCurrency(13.37);
    assertThat(formatted).isEqualTo("13,37 €");
}

With Java 8 the test is successful. However, with Java 11 you get the following error:

Expecting:
 <"13,37 €">
to be equal to:
 <"13,37 €">
but was not.

What?

Okay, solution: With Java 8 a normal space (ASCII code 0x20) was put between number and currency symbol. With Java 11, a non-breaking space (ASCII code 0xA0) is used. They both look the same, but they are not the same.

This change in the JDK then has the effect that formatCurrency(13.37).replace(" €", "") still returns the string "13.37 €".

Conclusion

These specific cases were deeply hidden in the system. After a Java update, no one would have noticed these changes, neither in a local test, nor in a customers' test system. This would be a typical candidate for "bugs that first become visible in the production system". Fortunately, in both cases there were unit tests for the affected parts.

Unit tests are always a good way to ensure that a function works as expected by the developer. Without unit tests you will only notice very late or too late when something does not work as expected.

By the way, if you try out the code you wrote, you could convert this "trying out" directly into tests, couldn't you? Then the testing is also much faster, even if other developers want to try something.

Comments