Skip to content

Write tests

Tests

How confident are you in the code you work on? Will it do the things it is supposed to do as expected? Do you think that you can confidently modify this code without breaking any of its current functionalities? If this code is not properly tested, you probably don't, and shouldn't. If it is actually tested then maybe you can trust it.

Testing the code we write is one of the most valuable tool of software engineering. Tests help to ascertain that the functionalities of our code work as expected, they help to find out bugs even before they happen, they help make our code easy to change and they will even directly influence the design of your software, especially if you use Test Driven Development.

Manual tests

Manual tests are the first kind of tests we do and they are commonly used. When you develop something new you try to make it run, manually. Once it looks like it's working you are happy and you might decide to move on. However, you can only test a limited amount of cases like this before it gets too expensive and time consuming. As your codebase grows you probably won't test manually all its older functionalities, but only the one you are currently working on. This is why we advocate to adopt automated tests as the first tests to implement.

Automated tests

Automated tests are tests that are executed by a computer and thus they can be part of a development pipeline. For now, we will only talk about unit tests which are used to test the low-level functionalities of your code, the direct results of the implementation your are working on. In Nexus-e we use Python pytest framework, a pytest test looks like this:

class TestValueFormat:
    def test_truncate_value_to_decimal(self):
        # Arrange
        sut = ValueFormatter(0.12345)
        truncated_decimal = 3
        expected_result = 0.123

        # Act
        result = sut.truncate(decimal=truncated_decimal).get_formatted_value()

        # Assert
        assert result == expected_result

To apply different test cases on the same test you can parametrize the test:

class TestValueFormat:
    truncate_value_data = [
        (0.12345, 3, 0.123),
        (0.1234567, 6, 0.123456),
        (0.12, 3, 0.12),
    ]

    @pytest.mark.parametrize(
        "value, decimal, expected_result", truncate_value_data
    )
    def test_truncate_value_to_decimal(self, value, decimal, expected_result):
        # Arrange
        sut = ValueFormatter(value)

        # Act
        result = sut.truncate(decimal=decimal).get_formatted_value()

        # Assert
        assert result == expected_result

Such test shouldn't replicate the logic of the production code which means the expected result should be hard-coded or read from a source and not obtained with the same algorithm that runs in the code you want to test.

F.I.R.S.T. principles of testing

These principles help to write clean tests:

Fast

The execution of the tests should be fast. If it's not, you will likely avoid executing your tests too often. If you don't run your tests often enough you will let bugs spread and you will be less confident in modifying the codebase, countering the benefits of testing.

Independent

Tests should be independent from each other. One test shouldn't define the conditions of execution of another test. We should be able to run our tests in any given order.

Repeatable

Tests should be deterministic and their results shouldn't be influenced by the environment they run in. Each test should set up its own data and should not depend on any external factors to run.

Self-validating

Tests should provide a binary result: either the test pass or it fails. We shouldn't need to analyze some data produced by a test to determine if it is successful.

Timely

Tests should be written at the appropriate time, which is right before writing the actual production code.

Arrange, Act, Assert

It is a proven practice to divide a test in three parts.

Arrange

In this part you setup your System Under Test (SUT). It can implies instanciating the component you want to test and the other variables needed in the test.

Act

In this part you run the functionality you want to test. A test should focus on only one functionality. At this point you might obtain a result from the tested functionality.

Assert

Here you assert that the computed result corresponds to an expected result. You might also assert that some expected behavior have been observed (calling a method, raising an exception, ...).

Where should I write my tests

In the root folder of nexus-e-framework exists a "tests" folder. Write your unit tests in files like test>unit_tests>test_my_component.py. The organization of the tests files should reproduce the organization of the production code.