I have 10 or 20 tests with the same structure which I would like to improve. The test itself is nested within a try/except/finally
clause to make sure that some of the variables are deleted at the end.
def test_time_correction():
"""Test time_correction method."""
sinfo = StreamInfo("test", "", 2, 0.0, "int8", uuid.uuid4().hex[:6])
try:
outlet = StreamOutlet(sinfo, chunk_size=3)
inlet = StreamInlet(sinfo)
inlet.open_stream(timeout=5)
tc = inlet.time_correction(timeout=3)
assert isinstance(tc, float)
except Exception as error:
raise error
finally:
try:
del inlet
except Exception:
pass
try:
del outlet
except Exception:
pass
In the example above, I have 2 variables inlet
and outlet
, but in other tests, the finally clause might be:
finally:
try:
del outlet1
except Exception:
pass
try:
del outlet2
except Exception:
pass
try:
del outlet3
except Exception:
pass
Open to suggestion on how this could be improved, maybe through fixture or other means.
-
1\$\begingroup\$ The current question title, which states your concerns about the code, is too general to be useful here. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How to get the best value out of Code Review: Asking Questions for guidance on writing good question titles. \$\endgroup\$Toby Speight– Toby Speight2023年06月30日 11:28:25 +00:00Commented Jun 30, 2023 at 11:28
1 Answer 1
Python uses garbage collection. Objects will be deleted automatically when they are no longer referenced. The del
statement doesn't force the object to be destroyed, it just removes the variable from the scope.
What your code achieves is extremely niche: if del
destroys the object immediately and the object's destructor raises an exception then this exception will be ignored, rather than failing or crashing the tests.
This is extremely niche because since Python is garbage collected, it doesn't have any kind of deterministic destruction. I have never seen a __del__()
method (destructor) in practice, even though Python supports this feature, and it might be useful when integrating native code.
See also these SO questions:
The latter post explains that if you have to perform cleanup in your code, then you should use the with
statement, for which objects have to provide __enter__()
and __exit__()
methods per the context manager protocol.
All of this means that your test is likely equivalent to:
def test_time_correction():
"""Test time_correction method."""
sinfo = StreamInfo("test", "", 2, 0.0, "int8", uuid.uuid4().hex[:6])
outlet = StreamOutlet(sinfo, chunk_size=3)
inlet = StreamInlet(sinfo)
inlet.open_stream(timeout=5)
tc = inlet.time_correction(timeout=3)
assert isinstance(tc, float)
But it could make sense to use with-statements. For example:
def test_time_correction():
"""Test time_correction method."""
sinfo = StreamInfo("test", "", 2, 0.0, "int8", uuid.uuid4().hex[:6])
with sinfo.outlet(chunk_size=3) as outlet, sinfo.inlet() as inlet:
inlet.open_stream(timeout=5)
tc = inlet.time_correction(timeout=3)
assert isinstance(tc, float)
Pytest also has a feature for setting up and tearing down resources for a test, called fixtures. This feature is documented here: https://docs.pytest.org/en/stable/explanation/fixtures.html
A fixture is a separate function that creates and tears down a value that is needed by a test function. Fixtures are invoked by giving the test function a parameter with a corresponding name. This is mostly useful if the same kind of object is needed for multiple test cases.
For example:
@pytest.fixture
def sinfo():
sinfo = StreamInfo("test", "", 2, 0.0, "int8", uuid.uuid4().hex[:6])
yield sinfo
clean_up(sinfo)
@pytest.fixture
def inlet(sinfo):
inlet = StreamInlet(sinfo)
inlet.open_stream(timeout=5)
yield inlet
clean_up(inlet)
@pytest.fixture
def outlet(sinfo):
outlet = StreamOutlet(sinfo, chunk_size=3)
yield outlet
clean_up(outlet)
def test_time_correction(inlet, outlet):
"""Test time_correction method."""
tc = inlet.time_correction(timeout=3)
assert isinstance(tc, float)
-
\$\begingroup\$ Thank you for the detail answer and the examples. I am aware of the unreliable behavior of
del
in Python, and that you can somehow force garbage collection through thegc
module. In this case, deletion calls a C++ lib destructor, and exceptions are purely ignored; thus thedel
approach is working reasonably well. Context manager would not suit my needs in this case, but I agree it makes a lot of sense. Fixtures seems like the way to go in my case, although they imply a large refactor of the unit test and are not that handy to work with IMO. \$\endgroup\$Mathieu– Mathieu2023年06月30日 11:39:40 +00:00Commented Jun 30, 2023 at 11:39 -
\$\begingroup\$ What would the
clean_up
function look like in the fixture example? \$\endgroup\$Mathieu– Mathieu2023年06月30日 11:40:05 +00:00Commented Jun 30, 2023 at 11:40 -
\$\begingroup\$ @Mathieu If there are C++ destructors involved then this is one of those rare cases where Python destructors could be relevant as well. With CPython (the default Python implementation), garbage collection happens to be fairly deterministic because it primarily uses reference counting. If these tests are supposed to cover the destructors then I might write code similar to your original tests, though I think the surrounding try-except doesn't really change much here. Using Pytest Fixtures probably won't quite help, but I wanted to mention them for completeness. \$\endgroup\$amon– amon2023年06月30日 12:01:59 +00:00Commented Jun 30, 2023 at 12:01