The Importance of Writing Tests

Reading time5 min

Important takeaways

Tests and documentation together ensure the long-term robustness and sustainability of the software.

  • Writing tests ensures that the software performs as expected and quickly identifies bugs and regressions.
  • It encourages developers to think critically about the code design and functionality, leading to cleaner, more reliable, and robust code.
  • Automated tests facilitate maintenance by quickly detecting issues that need fixing.
  • Automated tests are integral to continuous integration (CI) and continuous deployment (CD) pipelines, ensuring that new changes are consistently tested and integrated without introducing new issues.

Introduction

Writing tests for code is an essential practice in software development, as it ensures that each part of the program functions as expected. Automated tests, whether unit, integration, or system tests, quickly detect bugs and regressions, facilitating maintenance and improving the overall quality of the software. Besides error detection, tests boost developers’ confidence in making changes to the code and make the refactoring process smoother. Furthermore, comprehensive testing promotes better code design by encouraging developers to consider edge cases and potential failure points early in the development process. It also supports continuous integration and deployment (CI/CD) practices, ensuring that new code changes are thoroughly vetted before being integrated into the main codebase. In summary, writing tests is a proactive measure that enhances software reliability, maintainability, and development efficiency.

Testing your code

There are several types of tests used in software development, each serving a specific purpose and targeting different aspects of the software. Here are some common types of tests:

  • Unit Tests: Unit tests are focused on testing individual units or components of the software, such as functions, methods, or classes, in isolation from the rest of the application. They verify that each unit functions correctly according to its specification.

  • Integration Tests: Integration tests verify that different units or components of the software work together correctly when integrated. They test the interactions between these units and ensure that they communicate and collaborate as expected.

  • Regression Tests: Regression tests are aimed at ensuring that changes made to the codebase, such as bug fixes or new features, do not introduce new defects or regressions into the software. They help maintain the stability and reliability of the application over time.

  • Security Tests: Security tests assess the security of the software and identify potential vulnerabilities or weaknesses that could be exploited by attackers. They help ensure that the software is protected against security threats and breaches.

By using a combination of these tests throughout the software development lifecycle, teams can ensure that the software meets quality standards, performs reliably, and satisfies user needs. Here, we will concentrate on unit tests.

A unit test is a type of software testing that focuses on verifying the correctness of individual units of code. A “unit” is the smallest testable part of an application, such as a function, method, or class. The primary goal of unit testing is to ensure that each unit performs as expected in isolation from the rest of the application.

Writing tests may seem like a slow process at first glance, but they are essential for delivering a program on time and on budget. Tests help to detect bugs early in the development cycle, reducing the cost of correcting errors. They also allow the code to be refactored with confidence and help the code to be understood. Test frameworks allow you to write tests quickly.

unittest and JUnit

As indicated in the unittest documentation, unittest and JUnit are close:

The unittest unit testing framework was originally inspired by JUnit and has a similar flavor as major unit testing frameworks in other languages.

To see how to build tests, here is a function that invert the capitalization of a word:

def inverse_capitalization(word: str) -> str:
    result = []
    for char in word:
        result.append(char.lower() if char.isupper() else char.upper())
    return ''.join(result)
public String inverseCapitalization(String word){
    char[] result = new char[word.length()];
    for (int i = 0; i < word.length(); i++) {
        char c = word.charAt(i);
        result[i] = Character.isUpperCase(c) ? Character.toLowerCase(c) : Character.toUpperCase(c);
    }
    return new String(result);
}

In order to ensure that the method does what is expected of it, a test class has been written:

import unittest


class TestInverseCapitalization(unittest.TestCase):

    def test_lower(self):
        self.assertEqual('HELLO!', inverse_capitalization('hello!'))

    def test_upper(self):
        self.assertEqual('hello!', inverse_capitalization('HELLO!'))

    def test_mix(self):
        self.assertEqual('hElLo!', inverse_capitalization('HeLlO!'))    


if __name__ == '__main__':
    unittest.main()
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestInverseCapitalization {
    
    @Test
    public void testUpper() {
        Assertions.assertEquals("hello!", inverseCapitalization("HELLO!"));
    }
    
    @Test
    public void testLower() {
        Assertions.assertEquals("HELLO!", inverseCapitalization("hello!"));
    }

    @Test
    public void testMix() {
        Assertions.assertEquals("HeLlO!", inverseCapitalization("hElLo!"));
    }
}

This class of tests, called TestInverseCapitalization, contains three tests. Only one concept is evaluated per test and only one test is created per concept. Note that each method that tests a feature starts with test_. For example, the first test ensures that a word written entirely in lower case is correctly transformed into a word written entirely in upper case. The test itself consists of a call to the function assertEqual(expected, actual, msg=None). It tests whether expected and actual are equal, and it will fail if they are not. Furthermore, it guarantees that expected and actual are of the same type.

Run this test class to confirm that the concepts have been correctly implemented, indicating any bugs through failed tests.

By running the unittest.main(), all methods in the TestInverseCapitalization class that start with test_ are executed. The results are displayed in the terminal, showing which tests passed and which failed.

The testing frameworks provide a set of ready-to-use methods that simplify tests elaboration: unittest assert methods and JUnit assertions.

A good practice is to have the tests written before the code is written by anyone other than the person who will be developing the code:

  • Tests written beforehand serve as a clear specification of the expected behavior,
  • The person writing the tests focuses on what the code should achieve without being influenced by the implementation details,
  • It promotes a development approach driven by functionality and user needs,
  • It encourages communication and ensures that both parties have a shared understanding of the project goals.