**Approach (brief)** I’ll show two robust pytest fixture patterns that create an external resource (e.g., a test DB or cloud object), yield it to tests, and guarantee cleanup: (1) yield-style fixture with teardown after yield, and (2) fixture that registers a finalizer via request.addfinalizer. I’ll also show using a context-manager helper for idempotent create/delete and explain idempotency considerations.Code: yield-style with contextmanager and finalizer examplepython
import pytest
from contextlib import contextmanager
import uuid
import time
# Simulated external resource API (replace with real cloud/db client)
class ExternalObjClient:
def create(self, name):
# create resource and return identifier
return {"id": name, "created": time.time()}
def delete(self, id):
# delete resource; raising on missing simulates transient failures
# In real client, handle 404 as success (idempotent delete)
pass
client = ExternalObjClient()
@contextmanager
def external_resource(name_prefix="test-res"):
name = f"{name_prefix}-{uuid.uuid4().hex[:8]}"
res = client.create(name) # create
try:
yield res
finally:
# idempotent cleanup: swallow not-found errors, retry transient failures
for attempt in range(3):
try:
client.delete(res["id"])
break
except Exception as e:
if attempt == 2:
# log and re-raise if unrecoverable
raise
time.sleep(0.5)
# 1) Yield-style fixture (preferred simple pattern)
@pytest.fixture
def ext_res_yield():
with external_resource() as res:
yield res # teardown runs after test even if it fails/raises
# 2) Request.finalizer style
@pytest.fixture
def ext_res_finalizer(request):
name = f"test-res-{uuid.uuid4().hex[:8]}"
res = client.create(name)
def _cleanup():
# idempotent delete
try:
client.delete(res["id"])
except Exception:
pass
request.addfinalizer(_cleanup)
return res
Why these guarantee cleanup- Yield fixtures: pytest always runs the code after yield even if test raises; this is the recommended idiom for setup/teardown.- request.addfinalizer: registers a teardown callback run after the test, independent of test outcome.- Both execute in-process; if an external plugin forcibly kills process (rare), consider out-of-process watchdog or CI job cleanup.Idempotency & best practices- Make delete operations idempotent: treat "not found" as success.- Retry transient errors with backoff; limit retries to avoid hanging CI.- Use unique names (UUID) to avoid collisions and safe concurrent tests.- Centralize create/delete in helper functions so cleanup logic is consistent across fixtures.- For timeouts: pytest-timeout may interrupt tests but still runs finalizers; however, if timeouts kill the process externally, implement separate garbage-collector jobs (scheduled cleanup) or a TTL on external resources.Example test usagepython
def test_using_resource(ext_res_yield):
assert "id" in ext_res_yield
# test logic that may raise or timeout; teardown still runs