TDD in practice

Wojciech Maziarz

Introduction

Test Driven Development is one of fundamental practices in the Agile methodology. All of us have already heard the term TDD many times. But how it looks like in a real life? Does it make sense to obey all TDD rules? What are benefits of writing unit tests? Is it about statistics of code coverage only? What does code coverage really mean? I will try to share my impressions regarding this questions in this article.

Write a test together with an application code

All of us have heard that according to TDD unit test should be written before the code under test is written. And almost none of us obey this rule. To be honest I don’t find this deviation as something wrong because writing a test before a class/method being under test is often impossible.

If the method accepts some arguments then performs some calculations and returns the result and does not need to call any other classes, then yes, writing a test before implementation is a reasonable approach – see the example below:

public long calculateDaysBetween(String fromDay, String toDay) {
   DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
   LocalDate from = LocalDate.parse(fromDay, dtf);
   LocalDate to = LocalDate.parse(toDay, dtf);

   return ChronoUnit.DAYS.between(from, to);
}

On the other hand in the example below there are two collaborating objects used (userRepository, emailSender) and the method returns void. So how to write a proper test before implementation of this method is ready? How could we know what to mock if we didn’t have an implementation of the class under test yet?

public void sendEmailsToUsers(String subject, String content, String userNameLike) {
   userRepository.findAll().stream()
         .filter(u -> isUserMatching(u, userNameLike))
         .map(User::getEmail)
         .forEach(emailAddr -> emailSender.sendEmail(subject, content, emailAddr));
}

Going to conclusion, in a real life it is hard to follow the rule of writing test before class. Anyway test should be written as soon as the class under test is ready. I can bet that all of us at least once or even more said: I have to write the code and push it because of deadlines, because other team members are waiting for my job to be completed, and I will write unit tests later if I have free capacity. But the true is that ‘later’ may never come. If we intend to write tests later we probably won’t do it at all.

Moreover, if you write a real unit test that adds some value (not just a happy path) together with the code you will write the code that is testable for sure. Because the code that is not testable or requires writing some very complex huge unit test is a badly designed code. Let’s assume that your teammate created some complex class ‘BigFatService’ having a very long method with many if –statements and pushed it without any unit tests. Then after a month or two you became a lucky guy who has free capacity and your scrum master says: Hey buddy, you are not busy now, so please write some missing unit tests for the ‘BigFatService’. You pull the code with 5 – level if – statements and try to write some valuable unit tests for it… Does this scenario sound familiar to you?

So the good practice is to push the code to the repository together with unit tests. Otherwise unit tests will never be written or they will be created eventually but their value will be low.

Test coverage

Some validation of the test coverage could help to force programmers to write unit tests in the time of writing the application code. So that if the coverage is too low, a build process on Jenkins (or other tool) will fail. Prior to pushing the code, programmer may run all tests on his local machine with some code coverage checking tool. There are some plugins available for different IDEs. For example in Intellij IDEA test coverage percentages may be shown in the project tree view and in the class  – covered parts are marked green, missed parts are marked red on the left of the file view:

Refactoring supported by unit tests

Unfortunately it is not always the case that we create the application from scratch, the code is brand new, newest technologies and newest versions of libraries are put in the pom.xml. Sometimes we have to work with some legacy code, or even if the code is not very old, some maintenance, bug fixing or implementing new features are required. We should treat this as an opportunity of leaving the code in a better condition than we got it.

If I see any class that has over 1000 lines, method having 3 levels of if – statements included in 3 nested loops, it means that I have to improve this code for sure. But how to do it properly and not lose any business logic? At first I have to understand this overcomplicated code. Then write some unit test for the part of code that I have just understood and do the same with other parts of code. If all class is covered by unit tests that I understand to, then I am ready to start refactoring. Move some methods to other classes, split long methods into smaller pieces. And after that exercise the test to check if the refactoring didn’t break any functionality.

If unit test I have just created do cover all lines of the code being refactored and the tests are passing after the refactoring it means that this part of the code works as it worked before the refactoring. Now I am ready to implement the required changes in the logic and of course to write additional unit test for each newly created part of the code.

Summary

TDD in a daily programmer work helps to write a code of higher quality and makes programmers feel more confident. Really.

There are only few simple rules to follow:

  • Write unit tests together with the application code
  • Test all logical paths and check it with code coverage report
  • Do refactoring and remember about unit tests

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑people!

Dołącz do nas