Testing¶
In DeepReg, we use pytest (not unittest) for unit tests to ensure a certain code quality and to facilitate the code maintenance.
The testing is checked via Travis-CI and Codecov is used to monitor the test coverage. While checking the Codecov report in file mode, generally a line highlighted by red means it is not covered by test. In other words, this line has never been executed during tests. Please check the Codecov documentation for more details about their coverage report.
Following are the guidelines of test writing to ensure a consistency of code style.
Coverage requirement¶
Class¶
For a class, we need to test all the functions in the class.
Non-TensorFlow function¶
For a non-TensorFlow function, we need to test
The correctness of inputs and the error handling for unexpected inputs.
The correctness of outputs given certain inputs.
The trigger of all errors (
ValueError
,AssertionError
, etc.).The trigger of warnings.
Check test_load_nifti_file()
in
test_nifti_loader.py
as an example.
TensorFlow function¶
For a TensorFlow-involved function, we need to test
The correctness of inputs and the error handling for unexpected inputs. The minimum requirement is to check the shape of input tensors.
The correctness of outputs given certain inputs if the function involves mathematical operations. Otherwise, at least the output tensor shapes have to be correct.
The trigger of all errors (
ValueError
,AssertionError
, etc.).The trigger of warnings.
Check test_resample()
in
test_layer_util.py
as an example.
Helper functions¶
As we are comparing often the numpy arrays and TensorFlow tensors, two functions
is_equal_np
and is_equal_tf
are provided in
test/unit/util.py.
They will first convert inputs to float32 and compare the max of absolute difference
with a threshold at 1e-6. They can be imported using
from test.unit.util import is_equal_np
so that we do not need one copy per test file.
Example unit test¶
In this section, we provide some minimum examples to help the understanding.
Function to be tested¶
Assuming we have the following function to be tested:
import logging
def subtract(x: int) -> int:
"""
A function subtracts one from a non-negative integer.
:param x: a non-negative integer
:return: x - 1
"""
if not isinstance(x, int):
raise ValueError(f"input {x} is not int")
assert x >= 0, f"input {x} is negative"
if x == 0:
logging.warning("input is zero")
return x - 1
Coverage requirement¶
The test should be as follows:
Name should be the tested function/class with prefix
test_
, in this example, it istest_subtract
.All cases are separated by a comment briefly explaining the test case.
Test a working case, e.g. input is 0 and 1.
Test a failing case and the
assert
, e.g. input is -1. We need to catch the error and check the error message if existed.Test the
ValueError
, e.g. input is 0.0, a float. We need to catch the error and check the error message if existed.Verify the warning is trigger, e.g. input is 0.
Test code¶
import pytest
def test_subtract(caplog):
"""test subtract by verifying its input and outputs"""
# x = 0
got = subtract(x=0)
expected = -1
assert got == expected
# x > 0
got = subtract(x=1)
expected = 0
assert got == expected
# x < 0
with pytest.raises(AssertionError) as err_info:
subtract(x=-1)
assert "is negative" in str(err_info.value)
# x is not int
with pytest.raises(ValueError) as err_info:
subtract(x=0.0)
assert "is not int" in str(err_info.value)
# detect caplog
caplog.clear() # clear previous log
subtract(x=0)
assert "input is zero" in caplog.text
# incorrect warning test example
# caplog.clear() # uncomment this line will fail the test
subtract(x=1) # this line generates no warning
assert "input is zero" in caplog.text
# incorrect error test example
with pytest.raises(AssertionError) as err_info:
# this line will trigger the Assertion Error
# comment the following line will fail the test
subtract(x=-1)
# the following line will never be executed
subtract(x=0.0)
assert "is negative" in str(err_info.value)
Common errors¶
Forget to clear caplog
In the example above, we are testing warning messages with
pytest caplog fixture. All the
messages are captured in caplog
which is the input argument of the test function. Be
careful that it is important to clear the caplog using caplog.clear
. Otherwise, as the
log is accumulated, we might have unexpected performance.
For instance, with the example above:
# incorrect warning test example
# caplog.clear() # uncomment this line will fail the test
subtract(x=1) # this line generates no warning
assert "input is zero" in caplog.text
The test will pass but the assertion works only because we have generated. If the
caplog.clear()
is uncommented, the test will fail.
Test multiple errors together
When testing errors, the assert
should be outside of the
with pytest.raises(ValueError) as err_info:
and we should not put multiple tests
inside the same with
as only the first error will be captured.
For instance, in the following test example, the second subtract will never be executed regardless of whether it is correct or not. If this trigger an error, it will never be captured.
# incorrect error test example
with pytest.raises(AssertionError) as err_info:
# this line will trigger the Assertion Error
# comment the following line will fail the test
subtract(x=-1)
# the following line will never be executed
subtract(x=0.0)
assert "is negative" in str(err_info.value)
The test will pass because the first subtract raises the desired assertion error. The test will fail if we comment out the first subtract.