← Назад к вопросам

Как сделать очистку ресурсов после выхода из теста в pytest?

2.0 Middle🔥 181 комментариев
#Python Core#Тестирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Очистка ресурсов после выхода из теста в pytest

Тесты часто создают временные ресурсы (файлы, БД, сокеты, подключения). Их нужно аккуратно очищать, чтобы избежать утечек и побочных эффектов между тестами. Расскажу о методах.

Метод 1: Fixtures с yield (рекомендуется)

import pytest
import tempfile
import os

@pytest.fixture
def temp_file():
    """Fixture, которая создаёт файл и удаляет его после теста"""
    # Setup — создание ресурса
    temp_fd, temp_path = tempfile.mkstemp()
    os.close(temp_fd)
    
    print(f"\nCreated temp file: {temp_path}")
    yield temp_path  # Тест получает путь
    
    # Teardown — очистка ресурса
    if os.path.exists(temp_path):
        os.remove(temp_path)
    print(f"\nDeleted temp file: {temp_path}")

def test_write_to_file(temp_file):
    """Тест использует fixture"""
    with open(temp_file, "w") as f:
        f.write("test data")
    
    # Файл автоматически удалится после теста
    assert os.path.exists(temp_file)

# Вывод:
# Created temp file: /tmp/xxx
# PASSED
# Deleted temp file: /tmp/xxx

Метод 2: Fixture с scope (для переиспользования между тестами)

import pytest
import tempfile
import shutil

@pytest.fixture(scope="module")
def temp_dir():
    """Создать директорию на время всего модуля"""
    temp_path = tempfile.mkdtemp()
    print(f"\nCreating {temp_path} for entire module")
    
    yield temp_path
    
    # Очистить после всех тестов модуля
    shutil.rmtree(temp_path)
    print(f"\nDeleted {temp_path}")

def test_one(temp_dir):
    file_path = f"{temp_dir}/file1.txt"
    with open(file_path, "w") as f:
        f.write("data1")
    assert os.path.exists(file_path)

def test_two(temp_dir):
    # Та же директория!
    file_path = f"{temp_dir}/file2.txt"
    with open(file_path, "w") as f:
        f.write("data2")
    assert os.path.exists(file_path)

Элементы scope:

  • function (default) — новый ресурс для каждого теста
  • class — один ресурс на класс тестов
  • module — один ресурс на модуль
  • session — один ресурс на всю сессию

Метод 3: Pytest fixtures с autouse

import pytest

@pytest.fixture(autouse=True)
def cleanup():
    """Автоматически запустится перед каждым тестом"""
    print("\nSetup: initializing resources")
    yield  # Тест выполняется
    print("\nTeardown: cleaning resources")

def test_one():
    print("Running test_one")
    assert True

def test_two():
    print("Running test_two")
    assert True

# Вывод:
# Setup: initializing resources
# Running test_one
# Teardown: cleaning resources
# Setup: initializing resources
# Running test_two
# Teardown: cleaning resources

Метод 4: Setup и Teardown методы в классах

import pytest

class TestDatabaseOperations:
    def setup_method(self):
        """Выполняется перед каждым методом"""
        self.db = DatabaseConnection(":memory:")
        self.db.create_tables()
        print("\nDatabase setup")
    
    def teardown_method(self):
        """Выполняется после каждого метода"""
        self.db.close()
        print("\nDatabase teardown")
    
    def test_insert(self):
        self.db.insert("users", {"id": 1, "name": "John"})
        assert self.db.count("users") == 1
    
    def test_update(self):
        self.db.insert("users", {"id": 1, "name": "John"})
        self.db.update("users", {"id": 1}, {"name": "Jane"})
        user = self.db.get("users", 1)
        assert user["name"] == "Jane"

class TestFileOperations:
    @classmethod
    def setup_class(cls):
        """Выполняется один раз перед всеми методами"""
        cls.temp_dir = tempfile.mkdtemp()
        print(f"\nCreating {cls.temp_dir}")
    
    @classmethod
    def teardown_class(cls):
        """Выполняется один раз после всех методов"""
        shutil.rmtree(cls.temp_dir)
        print(f"\nDeleted {cls.temp_dir}")
    
    def test_write(self):
        file_path = f"{self.temp_dir}/test.txt"
        with open(file_path, "w") as f:
            f.write("test")
        assert os.path.exists(file_path)

Метод 5: Context managers для управления ресурсами

from contextlib import contextmanager
import tempfile
import shutil

@contextmanager
def temporary_directory():
    """Context manager для временной директории"""
    temp_path = tempfile.mkdtemp()
    try:
        yield temp_path
    finally:
        shutil.rmtree(temp_path)

def test_with_context_manager():
    with temporary_directory() as temp_dir:
        file_path = f"{temp_dir}/file.txt"
        with open(file_path, "w") as f:
            f.write("test")
        assert os.path.exists(file_path)
    
    # После выхода из блока — всё очищено
    assert not os.path.exists(temp_dir)

Метод 6: Очистка подключений к БД

import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

@pytest.fixture
def db_session():
    """Fixture для тестирования с БД"""
    # Setup: создать БД
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    
    Session = sessionmaker(bind=engine)
    session = Session()
    
    yield session
    
    # Teardown: очистить
    session.close()
    engine.dispose()

def test_user_creation(db_session):
    user = User(name="John", email="john@example.com")
    db_session.add(user)
    db_session.commit()
    
    result = db_session.query(User).first()
    assert result.name == "John"

Метод 7: Очистка моков и патчей

from unittest.mock import patch, MagicMock
import pytest

@pytest.fixture
def mock_api():
    """Fixture с mocking"""
    with patch("requests.get") as mock_get:
        mock_get.return_value.json.return_value = {"status": "ok"}
        yield mock_get
        # После теста mock автоматически удалится

def test_api_call(mock_api):
    import requests
    response = requests.get("https://api.example.com")
    assert response.json()["status"] == "ok"
    mock_api.assert_called_once()

Метод 8: Обработка исключений при очистке

import pytest

@pytest.fixture
def resource_with_cleanup():
    """Fixture с обработкой ошибок при очистке"""
    resource = None
    try:
        # Setup
        resource = create_expensive_resource()
        yield resource
    except Exception:
        # Если тест упал, всё равно очистим
        if resource:
            safe_cleanup(resource)
        raise
    finally:
        # Гарантированная очистка
        if resource:
            cleanup_resource(resource)

def test_with_resource(resource_with_cleanup):
    # Даже если тест упадёт, очистка выполнится
    assert resource_with_cleanup is not None

Метод 9: Наблюдение за утечками ресурсов

import pytest
import gc
import tracemalloc

@pytest.fixture(autouse=True)
def memory_check():
    """Проверить утечки памяти"""
    tracemalloc.start()
    snapshot_before = tracemalloc.take_snapshot()
    
    yield
    
    snapshot_after = tracemalloc.take_snapshot()
    top_stats = snapshot_after.compare_to(snapshot_before, "lineno")
    
    # Показать top 3 утечки
    print("\nTop memory allocations:")
    for stat in top_stats[:3]:
        print(stat)
    
    tracemalloc.stop()

def test_memory_intensive_operation():
    data = [i for i in range(1_000_000)]
    assert len(data) == 1_000_000

Best Practices

  1. Используй yield вместо return в fixtures — гарантирует очистку
  2. Используй autouse=True для общих очисток — не нужно передавать в каждый тест
  3. Выбирай правильный scope — function, class, module или session
  4. Обрабатывай ошибки при очистке — используй try/finally
  5. Тестируй очистку — проверяй, что ресурсы действительно удаляются
  6. Используй context managers — удобно и безопасно
  7. Логируй операции setup/teardown — для отладки
  8. Не оставляй побочные эффекты — каждый тест должен быть независим
  9. Используй tmpdir и tmp_path fixtures из pytest — встроенные для временных файлов

Встроенные fixtures pytest

def test_with_tmpdir(tmpdir):
    """tmpdir — встроенная fixture для временных файлов"""
    file_path = tmpdir.join("test.txt")
    file_path.write("content")
    assert file_path.read() == "content"
    # Автоматически удалится после теста

def test_with_tmp_path(tmp_path):
    """tmp_path — более современная версия (pathlib)"""
    file_path = tmp_path / "test.txt"
    file_path.write_text("content")
    assert file_path.read_text() == "content"
    # Автоматически удалится

Правильная очистка ресурсов — это основа надёжных и быстрых тестов!