Python Unit Testing¶
This page provides technical guidance to developers writing unit tests for DM’s Python code base.
See Software Unit Test Policy for an overview of LSST Stack testing.
If you have legacy DM
suite-based code (code that sets up a
unittest.TestSuite object by listing specific test classes and that uses
lsst.utils.tests.run rather than
unittest.main()), please refer to tech note SQR-012 for porting instructions.
LSST tests should be written using the
unittest framework, with default test discovery, and should support being run using the pytest test runner.
If you want to jump straight to a full example of the standard LSST Python testing boilerplate without reading the background, read the section on memory testing later in this document.
This document will not attempt to explain full details of how to use
unittest but instead shows common scenarios encountered in the LSST codebase.
unittest example is shown below:
import unittest import math class DemoTestCase1(unittest.TestCase): """Demo test case 1.""" def testDemo(self): self.assertGreater(10, 5) class DemoTestCase2(unittest.TestCase): """Demo test case 2.""" def testDemo1(self): self.assertNotEqual("string1", "string2") def testDemo2(self): self.assertAlmostEqual(3.14, math.pi, places=2) if __name__ == "__main__": unittest.main()
The important things to note in this example are:
- If the test is being executed using python from the command line the
unittest.main()call performs the test discovery and executes the tests, setting exit status to non-zero if any of the tests fail.
- Test classes are executed in the order in which they appear in the test file.
In this case the tests in
DemoTestCase1will be executed before those in
- Test classes must, ultimately, inherit from
unittest.TestCasein order to be discovered, and it is recommended that
lsst.utils.tests.TestCasebe used as the base class when
afwobjects are involved. The tests themselves must be methods of the test class with names that begin with
test. All other methods and classes will be ignored by the test system but can be used by tests.
- If a test method completes, the test passes; if it throws an uncaught exception the test has failed.
Using a more advanced test runner¶
pytest is not currently installed as part of a LSST stack installation. It can be installed using conda install pytest or pip install pytest.
All tests should be written such that they are runnable using pytest, a policy adopted in RFC-69.
pytest provides a much richer execution and reporting environment for tests and can be used to run multiple tests files together.
pytest test discovery is much more flexible than that provided by
unittest, but LSST test files should not take advantage of that flexibility as it can lead to inconsistency in test reports that depend on the specific test runner.
In particular, care must be taken not to have free functions that use a
test prefix or non-
TestCase test classes that are named with a
Test prefix in the test files.
The normal way to run tests is to use scons or scons tests.
scons will then execute each of the tests and report any failures.
Currently tests are executed one at a time using python and checking the command exit status.
sconsUtils will be modified to call py.test directly with all Python test files (possibly with a different order each invocation).
It is therefore important that during this transition period developers check that tests run correctly with pytest by explicitly running py.test when creating or updating test files.
$ py.test tests/*.py
and to ensure that the order of test execution does not matter it is useful to sometimes run the tests in reverse order:
$ py.test `ls -r tests/*.py`
When writing tests it is important that tests are skipped using the proper
unittest skipping framework rather than returning from the test early.
unittest supports skipping of individual tests and entire classes using decorators or skip exceptions.
LSST code sometimes raises skip exceptions in
setUpClass() class methods.
It is also possible to indicate that a particular test is expected to fail, being reported as an error if the test unexpectedly passes.
Expected failures can be used to write test code that triggers a reported bug before the fix to the bug has been implemented and without causing the continuous integration system to die.
One of the primary advantages of using a modern test runner such as pytest is that it is very easy to generate machine-readable pass/fail/skip/xfail statistics to see how the system is evolving over time.
LSST Utility Test Support Classes¶
lsst.utils.tests provides several helpful functions and classes for writing Python tests that developers should make use of.
- Asserts that floating point scalars and/or arrays are equal within the specified tolerance.
The default tolerance is significantly tighter than the tolerance used by
numpy.testing.assert_almost_equal(); if you are replacing either of those methods you may have to specify
atolto prevent failing asserts.
- Asserts that floating point scalars and/or arrays are identically equal.
- Asserts that floating point scalars and/or arrays are not equal.
assertNotClose methods have been deprecated by the above methods.
afw provides additional asserts that get loaded into
lsst.utils.tests.TestCase when the associated module is loaded.
These include methods for Coords, Geom (Angles, Pairs, Boxes), and Images, such as:
- Assert that two coords represent nearly the same point on the sky (provided by
- Assert that two angles are nearly equal, ignoring wrap differences by default (provided by
- Assert that two planar pairs (e.g.
Extent2D) are nearly equal (provided by
- Assert that two boxes (
Box2I) are nearly equal (provided by
skyToPixelfor two WCS over a rectangular grid of pixel positions (provided by
- Assert that two images are nearly equal, including non-finite values (provided by
- Assert that two masks are equal (provided by
- Assert that two masked images are nearly equal, including non-finite values (provided by
In some cases the test to be executed is a shell script or a compiled binary executable.
In order for the test running environment to be aware of these tests, a Python test file must be present that can be run by pytest.
If none of the tests require special arguments and all the files with the executable bit set are to be run, this can be achieved by copying the file
$UTILS_DIR/tests/testExecutables.py to the relevant
The file is reproduced here:
1 2 3 4 5 6 7 8 9 10 11 12
import unittest import lsst.utils.tests class UtilsBinaryTester(lsst.utils.tests.ExecutablesTestCase): pass EXECUTABLES = None UtilsBinaryTester.create_executable_tests(__file__, EXECUTABLES) if __name__ == "__main__": unittest.main()
EXECUTABLES variable can be a tuple containing the names of the executables to be run (relative to the directory containing the test file).
None indicates that the test script should discover the executables in the same directory as that containing the test file.
The call to
create_executable_tests initiates executable discovery and creates a test for each executable that is found.
In some cases an explicit test has to be written either because some precondition has to be met before the test will stand a chance of running or because some arguments have to be passed to the executable.
To support this the
assertExecutable method is available:
def testBinary(self): self.assertExecutable("binary1", args=None, root_dir=os.path.dirname(__file__))
binary1 is the name of the executable relative to the root directory specified in the
root_dir optional argument.
Arguments can be provided to the
args keyword parameter in the form of a sequence of arguments in a list or tuple.
The LSST codebase is currently in transition such that
sconsUtils will run executables
itself as well as running Python test scripts that run executables.
Do not worry about this duplication of test running.
When the codebase has migrated to consistently use the testing scheme described in this section
sconsUtils will be modified to disable the duplicate testing.
Memory and file descriptor leak testing¶
lsst.utils.tests.MemoryTestCase is used to detect memory leaks in C++ objects and leaks in file descriptors.
MemoryTestCase should be used in all test files where
utils is in the dependency chain, even if C++ code is not explicitly referenced.
This example shows the basic structure of an LSST Python unit test module,
MemoryTestCase (the highlighted lines indicate the memory testing modifications):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
import unittest import lsst.utils.tests class DemoTestCase(lsst.utils.tests.TestCase): """Demo test case.""" def testDemo(self): self.assertNotIn("i", "team") class MemoryTester(lsst.utils.tests.MemoryTestCase): pass def setup_module(module): lsst.utils.tests.init() if __name__ == "__main__": lsst.utils.tests.init() unittest.main()
which ends up running the single specified test plus the two running as part of the leak test:
$ py.test -v unittest_runner_example.py ============================= test session starts ============================== platform darwin -- Python 2.7.11, pytest-2.8.5, py-1.4.31, pluggy-0.3.1 -- ~/lsstsw/miniconda/bin/python cachedir: .cache rootdir: .../coding/unit_test_snippets, inifile: collected 3 items unittest_runner_example.py::DemoTestCase::testDemo PASSED unittest_runner_example.py::MemoryTester::testFileDescriptorLeaks <- .../lsstsw/stack/DarwinX86/utils/12.0.rc1+f79d1f7db4/python/lsst/utils/tests.py PASSED unittest_runner_example.py::MemoryTester::testLeaks <- .../lsstsw/stack/DarwinX86/utils/12.0.rc1+f79d1f7db4/python/lsst/utils/tests.py PASSED =========================== 3 passed in 0.28 seconds ===========================
MemoryTestCase must always be the
final test suite.
For the memory test to function properly the
lsst.utils.tests.init() function must be invoked before any of the tests in the class are executed.
Since LSST test scripts are required to run properly from the command-line and when called from within pytest, the
init() function has to be in the file twice: once in the setup_module function that is called by pytest whenever a test module is loaded (pytest will not use the
__main__ code path), and also just before the call to
unittest.main() call to handle being called with python.
It is now commonplace for Unicode to be used in Python code and the LSST test cases should reflect this situation. In particular file paths, externally supplied strings and strings originating from third party software packages may well include code points outside of US-ASCII. LSST tests should ensure that these cases are handled by explicitly including strings that include code points outside of this range. For example,
- file paths should be generated that include spaces as well as international characters,
- accented characters should be included for name strings, and
- unit strings should include the µm if appropriate.