Writing tests

Reading time5 min

In brief

Article summary

In this article, we introduce testing, which aims at verifying that developed codes behave as expected. Automatization of performing tests is also useful to verify that a new functionality in a code did not break something in alredy existing code.

We also mention how to design such tests, and show examples with dedicated libraries.

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.

Article contents

1 — Testing your code

1.1 — The purpose of testing

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.

1.2 — What kind of test?

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.

2 — Unit tests

In this article, we will focus 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.

2.1 — Specialized libraries: 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 inverts the capitalization of a word:

def inverse_capitalization (word: str) -> str:

    """
        Inverts the capitalization of a word.
        For instance Hello should be transformed to hELLO.
        In:
            * word: The word to process.
        Out:
            * The word with inversed capitalization.
    """

    # Fill a list char by char
    result = []
    for char in word:
        result.append(char.lower() if char.isupper() else char.upper())
    
    # Recreate the string
    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.

Information

You have not encountered a lot of classes yet. A class is an abstract data type, that groups both attributes (data) and methods (functions).

To define a class, you need to define these elements in a class block. More details will be given about this in the dedicated programming session. For now, just assume you have to write your tests as in the example below.

# Needed imports
import unittest



# Define a class for all your tests of the unit you want to test
# Here, the unit is a single function, but it could be an entire module, or a class
# The name of your test class (here, TestInverseCapitalization) should represent what you will test
# For the particular case of unit tests, it should inherit from unittest.TestCase as follows
# This allows access to useful methods such as 'assertEqual'
class TestInverseCapitalization (unittest.TestCase):

    # Define a method that will test the method for uppercase inputs
    # The name of the method should be representative of the test, and start with test_
    # Also, as it is a method in a class, it should have 'self' as first argument
    def test_lower (self):

        # For tests, it is good to work with assertions
        # Here, assertEqual is provided by the unittest library
        # It is nearly equivalent to: assert 'HELLO!' == inverse_capitalization('hello!')
        # The library provides other methods for complex assertions
        self.assertEqual('HELLO!', inverse_capitalization('hello!'))

    # Another method for another test
    def test_upper (self):
        self.assertEqual('hello!', inverse_capitalization('HELLO!'))

    # Another method for another test
    def test_mix (self):
        self.assertEqual('hElLo!', inverse_capitalization('HeLlO!'))    



# In the main, you can then ask unittest to run all your defined tests
if __name__ == '__main__':
    _ = unittest.main(argv=[""], verbosity=2, exit=False)
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. This is more interesting than just having a list of asserts, as the library will generate a full report, which is better for debugging.

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

2.2 — Writing codes or tests first?

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.

To go further

When the code you want to test interacts with other components of your code, it is possible to simulate the part of the code you interact with. This is using the mock object library. It is particularly relevant when a function waits for a user input.

To go beyond