Unit testing is awesome

Testing is often seen as a tedious and time-consuming task by many developers. However, in this article, I aim to change that perspective. I firmly believe that tests are a fundamental part of the development process, and not only do they bring numerous benefits, but they also foster a positive attitude towards software quality. In this post, we will explore few of the many reasons why writing tests is important, with a focus on Spring Boot development. Along the way, we’ll provide some code examples to illustrate the basic concepts

Find bugs before they find you

One of the primary reasons to write tests is to catch bugs early in the development cycle. Tests act as a safety net, allowing you to validate your code’s correctness continuously. By writing unit tests using frameworks like JUnit and Spring Boot Test, you can isolate and verify the behaviour of individual components or methods.

Say we have a user service class which hold our user management functionality. We can write a simple test as shown below to to ensure that our createUser method does what it’s meant to do.

@SpringBootTest
public class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    public void testGetUserById() {
        // Arrange
        User user = new User("John", "Doe");
        userService.createUser(user);
        
        // Act
        User retrievedUser = userService.getUserById(user.getId());
        
        // Assert
        assertEquals(user, retrievedUser);
    }
}

If we made breaking change within our createUser method, we will not be able to catch that without deploying/running the app somewhere. Running unit tests would not take long and such breaking changes can easily be caught sooner.

Improved code quality

Unit tests are usually written to test smaller chunks of your code. This enforces good programming practices such as writing loosely-coupled and testable code.

For the purpose of illustration, say our createUser method had the following code. We validate the request, we then check if such user exists in our database, then save the user, send welcome email to them.

class UserService {
    private final UserRepository userRepository;
    private final JavaMailSender emailSender;

    public UserDTO createUser(CreateUserRequest request) {
        // validate request
        if (request== null) {
            log.error("User is null");
            throw new RuntimeException("User is null or empty");
        }

    ...

        // check user email is not already taken
         userRepository.findByEmail(request.getEmail()).ifPresent(r -> {
            log.error("User email is already taken");
            throw new RuntimeException("User email is already taken");
        });


        // save the user
        User user = new User();
    ...
        user.setPassword(request.getPassword());

        userRepository.save(user);

        // send welcome email
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom("noreply@mycompany.com");
        message.setTo(user.getEmail());
        message.setSubject("Subject here");
        message.setText("Welcome to my awesome app!");
        emailSender.send(message);

        return null;
    }
}

There is a lot going on that is not about just creating a user. If we want to test this method, there is more assertions we have to make. In additional, our test would fail if for example an exception was thrown when sending the welcome email.

To improve this, we would extract different parts of the code into different methods or even util classes. This means, if our utility method passes its own test then it would not make createUser method to fail. This in turn improves our code quality. Let’s see what our userService would look like after refactor.

class UserService {
    private final UserRepository userRepository;
    private final JavaMailSender emailSender;

    public UserDTO createUser(CreateUserRequest request) {
        // validate request
        validateUserRequest(request);

        // save the user
        User user = new User();
    ...
        user.setPassword(request.getPassword());

        userRepository.save(user);

        // send welcome email
        sendWelcomeEmail(user);

        return null;
    }

    private void validateUserRequest(CreateUserRequest user) {
   ...
    }

    private void sendWelcomeEmail(User user) {
   ...
    }
}

Documentation for developers

By reading a test method, a developer can get a brief overview of what a piece of code does. While the understanding may not be detailed, it still helps to get an idea of what is going on.

Tests can also help new team members quickly onboard and contribute. By understanding the intended behaviour, we can potentially find better ways to achieve the same goal.

Confidence

Another benefit of having tests is that it can give you confidence in your code. Automated tests can be run at any time, and this can give assurance that any changes made to the codebase did not break any existing code. This is especially true for regression testing, where tests that previously passed are run again to ensure that they still pass after changes have been made. With this in mind, it’s important to keep your tests up to date and relevant to the current codebase.