Skip to content
Snippets Groups Projects
README.md 6.88 KiB
Newer Older
# Unit Testing

Is the procedure of testing individual units of code for fit for use. Often
this entails isolating specific segments and testing the behavior given a
certain range of different inputs. Arguably, the most effective forms of unit
testing  deliberately uses _edge cases_ and makes assumptions about any other
sections of code that are not currently under test.

First the underlying technologies as well as how they are applied in Tango
Station Control are discussed. If this seems uninteresting, or you are already
familiar with these concepts you can skip to
[Running tox tasks](#running tox tasks) and
[Debugging unit tests](#debugging-unit-tests).

### Table of Contents:

- [Tox](#tox)
- [Testing approach](#testing-approach)
- [Mocking](#mocking)
- [Running tox tasks](#running-tox-tasks)
- [Debugging unit tests](#debugging-unit-tests)

## Tox

[Tox](https://tox.readthedocs.io/en/latest/) is a commandline tool to simplify
running Python tasks such as linting and unit testing. Using a simple
[configuration file](../tox.ini) it can setup a
[virtual environment](https://virtualenv.pypa.io/en/latest/) that automatically
installs dependencies before executing the task. These environments persist
after the task finishes preventing the excessive downloading and installation
of dependencies.

The Tox environments in this project are configured to install any dependencies
listed in [test-requirements.txt](../test-requirements.txt) and
[lofar-requirements.txt](../../docker-compose/lofar-device-base/lofar-requirements.txt)
this can also  easily be verified within our [configuration file](../tox.ini).

## Testing approach

For testing [stestr](https://stestr.readthedocs.io/en/latest/) is used this
tool  has main advantage the utilization of a highly parallelized unit test
runner that automatically scales to the number of simultaneous threads
supported by the host processor. Other features include automatic test
discovery and pattern matching to only execute a subset of tests.

However, stestr is incompatible with using breakpoints
(through [pdb](https://docs.python.org/3/library/pdb.html)) directly and  will
simply fail the test upon encountering one. The
[debugging unit tests](#debugging-unit-tests) section describes how to mitigate
this and still debug individual unit tests effectively.

All tests can be found in this test folder and all tests must inherit from
`TestCase`, found in [base.py](base.py). This ensures that any test fixtures run
before and after tests. These test fixtures allow to restore any global modified
state, such as those of static variables. It is the task of the developer
writing the unit test to ensure that any global modification is restored.

When writing unit tests it is best practice to mimic the directory structure of
the original project inside the test directory. Similarly, copying the filenames
and adding _test__ to the beginning. Below is an example:

* root
  * database
    * database_manager.py
  * test
    * base.py
    * database
        * test_database_manager.py

## Mocking

Contrary to many other programming languages, it is entirely possible to
modify **any** function, object, file, import at runtime. This allows for a
great deal of flexibility but also simplicity in the case of unit tests.

Isolating functions is as simple as mocking any of the classes or functions it
uses and modifying its return values such as specific behavior can be tested.
Below is a simple demonstration mocking the return value of a function using the
mock decorator. For more detailed explanations see
[the official documentation](https://docs.python.org/3/library/unittest.mock.html).

```python
from unittest import mock

# We pretend that `our_custom_module` contains a function `special_char`
import our_custom_module

def function_under_test(self):
    if our_custom_module.special_char():
        return 12
    else:
        return 0

@mock.patch.object(our_custom_module, "special_char")
def test_function_under_test(self, m_special_char):
    """ Test functionality of `function_under_test`

    This mock decorator _temporarily_ overwrites the :py:func:`special_char`
    function within :py:module:`our_custom_module`. We get access to this
    mocked object as function argument. Concurrent dependencies of these mock
    statements are automatically solved by stestr.
    """

    # Mock the return value of the mocked object to be None
    m_special_char.return_value = None

    # Assert that function_under_test returns 0 when special_char returns None.
    self.assertEqual(0, function_under_test())

```

## Running tox tasks

Running tasks defined in Tox might be a little different than what you are used
to. This is due to the Tango devices running from Docker containers. Typically,
the dependencies are only available inside Docker and not on the host.

The tasks can thus only be accessed by executing Tox from within a Tango device
Docker container. A simple interactive Docker exec is enough to access them:

```sh
docker exec -it device-sdp /bin/bash
cd /opt/lofar2.0/tango/devices/
tox
```

For specific tasks the `-e` parameter can be used, in addition, any arguments
can be appended after the tasks. These arguments can be interpreted within the
Tox  configuration file as `{posargs}`. Below are a few examples:

```sh
# Execute unit tests with Python 3.7 and only execute the test_get_version test
# from the TestLofarGit class found within util/test_lofar.py
# Noteworthy, this will also execute test_get_version_tag_dirty due to pattern
# matching.
tox -e py37 util.test_lofar.TestLofarGit.test_get_version_tag
# Execute linting
tox -e pep8
```

## Debugging unit tests

Debugging works by utilizing the
[virtual environment](https://virtualenv.pypa.io/en/latest/)that Tox creates.
These are placed in the .tox/ directory. Each of these environments carries
the same name as found in _tox.ini_, these match the names used for `-e`
arguments

Debugging unit tests is done by inserting the following code segment just before
where you think issues occur:

```python
import pdb; pdb.set_trace()
```

Now as said stestr will catch any breakpoints and reraise them so we need to
avoid using stestr while debugging. Simply source the virtual environment
created by tox `source .tox/py37/bin/activate`. You should now see that the
shell $PS1 prompt is modified to indicate the environment is active.

From here execute `python -m testtools.run` and optionally the specific test
case as command line argument. These test will not run in parallel but support
all other features such as autodiscovery, test fixtures and mocking.

Any breakpoint will be triggered and you can use the pdb interface
(very similar to gdb) to step through the code, modify and print variables.

Afterwards simply execute `deactivate` to deactivate the virtual environment.
**DO NOT FORGOT TO REMOVE YOUR `pdb.set_trace()` STATEMENTS AFTERWARDS**

The best approach to prevent committing `import pdb; pdb.set_trace()` is to
ensure that all unit tests succeed beforehand.