1
\$\begingroup\$

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.

asked Jun 30, 2023 at 9:24
\$\endgroup\$
1
  • 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\$ Commented Jun 30, 2023 at 11:28

1 Answer 1

5
\$\begingroup\$

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)
answered Jun 30, 2023 at 11:14
\$\endgroup\$
3
  • \$\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 the gc module. In this case, deletion calls a C++ lib destructor, and exceptions are purely ignored; thus the del 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\$ Commented Jun 30, 2023 at 11:39
  • \$\begingroup\$ What would the clean_up function look like in the fixture example? \$\endgroup\$ Commented 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\$ Commented Jun 30, 2023 at 12:01

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.