Testing#
The test suite must pass before a pull request can be merged, and tests should be added to cover all modifications in behavior.
Tests are located in the tests/ directory.
We also test examples in docstrings of our package (located in src/).
Dealing with RNG state#
Prefer creating local np.random.RandomState instances rather than using the
global NumPy RNG or np.random.default_rng. The RandomState class is
specifically intended for use in test code and the random number streams it
generates are guaranteed not to change.
Prefer using randomly generated but fixed RNG seeds. The
np.random.RandomState generator requires seeds between 0 and 2**32-1, so
you can use the following command-line helper to generate seeds:
python -c "import random; print(random.randint(0, 2**32-1))"
And in your test, create a random number generator like so:
def test_something():
# hard-code a seed randomly generated while writing the test
rng = np.random.RandomState(2376609660)
If you are comparing with known answers or if your test result otherwise depends on a specific RNG seed, add a comment to that effect.
For local development we use spin test which wraps the
pytest testing framework.
Examples of running spin test:
# All tests
spin test
# Doctests only (tests in docstrings)
spin test -- skimage _skimage2
# Tests inside directory(s)
spin test -- tests/skimage/morphology
spin test -- src/skimage/morphology tests/skimage/morphology
# Tests matching an expression
spin test -- -k threshold
# Combine above with test path to reduce test collection time
spin test -- tests/skimage/filters -k threshold
# Specific test
spin test -- tests/skimage/morphology/test_gray.py::test_3d_fallback_black_tophat
Tip
Arguments specified after the -- are forwarded as
options to pytest.
Testing requirements are listed in requirements/test.txt.
Note
CI runs only modified tests on pull requests
To keep feedback fast, CI workflows run spin test --test-modified on
pull requests, which limits the test run to subpackages that were changed
relative to the base branch. The full test suite still runs on pushes to
main and on merge-queue entries.
To force the full suite on a pull request — for example when changes affect test infrastructure rather than a specific subpackage — add the run-all-tests label to the PR.
You can also use --test-modified locally to replicate this CI behaviour:
# Run tests only for subpackages you have changed relative to your
# upstream tracking branch (e.g. origin/main)
spin test --test-modified
# Specify the base branch explicitly
spin test --test-modified --base-ref main
# Include doctests for modified subpackages
spin test --test-modified --doctest
spin test automatically detects whether scikit-image is installed as a
wheel (e.g. via spin install) or being tested from a meson build
directory. To test a pip-installed wheel, install it first and then run
spin test as usual:
spin install
spin test -- tests/skimage/morphology
# Combine with --test-modified to run only changed subpackages
spin test --test-modified
Warnings during testing phase#
By default, warnings raised by the test suite result in errors.
You can switch that behavior off by setting the environment variable
SKIMAGE_TEST_STRICT_WARNINGS to 0.
Test coverage#
Tests for a module should ideally cover all code in that module, i.e., statement coverage should be at 100%.
To measure test coverage run:
$ spin test --coverage
This will run tests and print a report with one line for each file in
skimage, detailing the test coverage:
Name Stmts Exec Cover Missing
------------------------------------------------------------------------------
skimage/color/colorconv 77 77 100%
skimage/filter/__init__ 1 1 100%
...
Multithreaded Testing#
scikit-image supports the free-threaded build and we endeavor to ensure the implementation is thread-safe.
pytest-run-parallel#
All tests are automatically run under pytest-run-parallel in the GitHub actions
CI using a free-threaded interpreter. The pytest-run-parallel plugin runs
each test in the entire test suite in a thread pool with many other instances of
the same test, simultaneously. This detects issues caused by use of global state
in the implementation of tested functionality. Since the thread pool runs the
same test several times across threads, the global state is shared, leading to
possible test failures.
Generally, the solution is to avoid using global state. For example, it is best
to avoid using the global NumPy RNGs exposed in the np.random
namespace. Instead, you should create and explicitly seed an RNG local to the
test. See Dealing with RNG state for more detail on dealing with RNGs.
Another example is a test that writes to a file. You should use the pytest tmp_path fixture rather than manually setting up temporary paths. This will automatically handle creating a thread-local temporary directory for each worker thread.
Sometimes using global state in a test is unavoidable. For example, Python module, function, and type objects are global state. That means tests that monkeypatch functionality are not thread-safe, however, sometimes monkeypatching is the most straightforward way to test something. In cases like this, you can mark a test as thread-unsafe using a pytest mark:
@pytest.mark.thread_unsafe(reason="Test mutates global plugin state")
def test_plugins():
...
This test will still run under a free-threaded interpreter, but it will execute on only one thread.
Another reason to mark tests as thread-unsafe is because a test spawns a thread or process pool. Usually, tests should only use only one level of parallelism to avoid CPU oversubscription.
Note that pytest-run-parallel marks many standard
library functions and built-in pytest fixtures as thread-unsafe
automatically. See the pytest-run-parallel README
for more information.
Explicitly multithreaded tests#
The threading module is part of the Python standard library. This means that
all Python classes can be used and mutated freely by a thread pool. If you are
working on the implementation of a mutable object, you should consider whether
it makes sense to allow shared mutation of the object under multiple threads and
whether it is safe to do so. Clearly document the thread safety guarantees of
the object. Also consider adding explciitly multithreaded tests to exercise code
paths that only fire under shared multithreaded use of an object.
See the Python free-threading guide for more information on test and document Python projects for multithreaded use.