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).
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
- [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`
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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.