Урок 11 из 12 · 2 минуты чтения

Окружение, импорты и тесты на pytest

Виртуальные окружения и зависимости, как работает импорт и чем опасны циклические импорты, __name__ == "__main__", pytest, фикстуры и mock.patch.

Виртуальное окружение

У каждого проекта — своя копия Python со своими пакетами. Проекты не конфликтуют версиями библиотек, системный Python не засоряется.

python -m venv .venv
source .venv/bin/activate     # Windows: .venv\Scripts\activate
pip install -r requirements.txt

Точные версии фиксируют lock-файлом — uv.lock, poetry.lock или хотя бы pip freeze, — иначе через полгода сборка соберёт другие версии. uv и poetry управляют окружением и зависимостями одной командой.

Как работает импорт

При первом import orders Python:

  1. ищет модуль по sys.path;
  2. выполняет его сверху вниз один раз;
  3. кладёт в sys.modules — повторные импорты берут оттуда.

Отсюда два правила. На уровне модуля не делают тяжёлого — подключений к базе, сетевых запросов: это выполнится при импорте. И циклические импорты ломаются: если a импортирует b, а b берёт имя из ещё не выполненного до конца a, получится ImportError: cannot import name. Лечат разделением модулей, импортом модуля вместо имени или импортом внутри функции. Импорт только для аннотаций — под if TYPE_CHECKING:.

if __name__ == "__main__"

def main():
    ...

if __name__ == "__main__":
    main()

При запуске python app.py у модуля __name__ равно "__main__", при импорте — его имя. Так один файл служит и модулем, и программой. Для multiprocessing со spawn (Windows, macOS) эта защита обязательна.

pytest

Тест — функция test_… в файле test_*.py, проверка — обычный assert: pytest сам покажет, какие значения не совпали.

import pytest

@pytest.mark.parametrize("price, discount, expected", [
    (100, 0, 100),
    (100, 0.1, 90),
])
def test_total(price, discount, expected):
    assert total([price], discount) == expected

def test_negative_discount():
    with pytest.raises(ValueError):
        total([100], -1)

pytest -x останавливается на первой ошибке, -k имя выбирает тесты. Тест проверяет одно поведение и не зависит от порядка запуска.

Фикстуры

Фикстура — функция с @pytest.fixture, результат которой pytest передаёт в тест по имени параметра. Код после yield — очистка:

@pytest.fixture(scope="session")
def db():
    conn = connect_test_db()
    yield conn
    conn.close()

def test_save(db):
    ...

scope задаёт, как часто фикстура создаётся заново: дорогое (контейнер базы) — раз на сессию, дешёвое и изменяемое — на каждый тест. Общие фикстуры кладут в conftest.py, их видно без импорта.

mock.patch — патчить там, где используют

patch временно заменяет атрибут по имени на Mock. Подменять нужно в модуле, где имя ищется, а не где оно определено:

# orders.py:  from payments import charge
with patch("orders.charge") as charge:      # не "payments.charge"
    charge.return_value = {"ok": True}
    place_order(...)
    charge.assert_called_once()

autospec=True создаёт мок с сигнатурой оригинала — неверный вызов упадёт. Для async-функций — AsyncMock. Много моков в тесте — признак сильной связанности: часто лучше фейковая реализация или интеграционный тест.

Что запомнить

  • У каждого проекта — свой venv и lock-файл.
  • Импорт выполняет модуль — тяжёлое на уровне модуля не делают.
  • assert, parametrize, pytest.raises — основа тестов.
  • Фикстуры — по имени параметра, очистка после yield.
  • patch — там, где имя используется.

Проверка урока

Войдите, чтобы проверить себя: сданные проверки всех уроков курса отмечают его темы пройденными на карте и в ИПР.

Вопросы по теме

Читайте также

Партнёр платформы

Разбор собеседования с sotto

После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.

Разбор собеседования