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.

\$\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\$ 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)
\$\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\$
    – Mathieu
    Jun 30, 2023 at 11:39
  • \$\begingroup\$ What would the clean_up function look like in the fixture example? \$\endgroup\$
    – Mathieu
    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
    Jun 30, 2023 at 12:01

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.