Найти в Дзене

[Python] Добавляем линтеры и тесты в проект

Эта статья является продолжением статьи Пишем простую программу на python и входит в цикл статей Пишем свою библиотеку с нуля и публикуем её в PyPI (GitVerse) В прошлой статье был написан код программы, но программа не тестировалась и не проверялась на линтеры и прочие чекеры. В этой статье мы быстро пробежимся по тестам, которые можно взять из git по ссылке. А после сделаем проверки тестов, покрытия, mypy, isort, pylint, vulture и black. ОС: Windows 10 Язык: Python 3.14 Вообще тесты надо писать во время кодинга и даже до кодинга основной программы. Но в рамках статьи я отошел от этого стандарта, чтоб все проверки разместить в одной статье. Все тесты размещаем рядом с нашей программой в папке \bnkc\tests. conftest.py — Конфигурация pytest test_calculator.py — Тесты бизнес-логики test_cli.py — Тесты CLI Чтоб не усложнять проект, у нас только 3 файла. Конечно, можно было бы добавить ещё тестов, добавить папки и раскидать по ним тесты, но решил не усложнять. Заходим в виртуальное окруж
Оглавление

Эта статья является продолжением статьи Пишем простую программу на python и входит в цикл статей Пишем свою библиотеку с нуля и публикуем её в PyPI (GitVerse)

В прошлой статье был написан код программы, но программа не тестировалась и не проверялась на линтеры и прочие чекеры.

В этой статье мы быстро пробежимся по тестам, которые можно взять из git по ссылке. А после сделаем проверки тестов, покрытия, mypy, isort, pylint, vulture и black.

Используемые технологии

ОС: Windows 10

Язык: Python 3.14

Тесты

Вообще тесты надо писать во время кодинга и даже до кодинга основной программы. Но в рамках статьи я отошел от этого стандарта, чтоб все проверки разместить в одной статье.

Структура тестов: что и зачем

Все тесты размещаем рядом с нашей программой в папке \bnkc\tests.

conftest.py — Конфигурация pytest

  • Единая точка конфигурации
  • Загружается до выполнения тестов
  • Тут можно добавить фикстуры, действия до/после тестов, конфигурировать плагины, менять настройки pytest

test_calculator.py — Тесты бизнес-логики

  • Используем фикстуры, так каждый тест получает чистый объект, не зависящий от других тестов.
  • Проверяем каждый метод отдельно
  • Используем маркеры, для группировки тестов

test_cli.py — Тесты CLI

  • Отдельный файл для тестирования при работе с консолью

Чтоб не усложнять проект, у нас только 3 файла. Конечно, можно было бы добавить ещё тестов, добавить папки и раскидать по ним тесты, но решил не усложнять.

Устанавливаем pytest

Заходим в виртуальное окружение и устанавливаем нужные библиотеки

cd bnkc
venv\Scripts\activate
py -m pip install pytest
py -m pip install pytest-cov
pip freeze > requirements.txt
deactivate

Дополним pyproject.toml

Добавляем блок опциональных зависимостей и создаем в нем раздел test.

[project.optional-dependencies]
test = [
"pytest>=9.0.2",
"pytest-cov>=7.0.0"
]

За счет этой строки можно будет производить установку зависимостей для теста командой

pip install -e ".[test]"

Добавляем блок [tool.pytest.ini_options]

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]

Указываем в блоке где искать тесты, как они должны именоваться.

В этом же блоке указываем дополнительные опции запуска для большей детализации вывода в консоль.

addopts = [
"--tb=short",
"-v",
]

В этом же блоке указываем маркеры, которыми мы декорировали скрипты в тестах.

markers = [
"unit: unit tests",
"integration: integration tests",
"credit: tests for Credit class",
"schedule: tests for PaymentSchedule",
"cli: tests for command line interface",
"calculation: calculation tests",
"statistics: statistics tests",
"validation: validation tests",
"parsing: parsing tests",
"edge_case: edge case tests",
"error_handling: error handling tests",
"smoke: smoke tests",
]

Добавляем блок [tool.coverage.run]

В нем указываем, что будем тестировать на покрытие, а что не будем.

[tool.coverage.run]
source = ["bnkc"]
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/test_*.py",
]

Добавляем блок [tool.coverage.report]

Добавляем для теста покрытия исключения на уровне кода.

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"raise AssertionError",
"raise NotImplementedError",
]

Запускаем тесты

Запускаем просто тесты из корня проекта

pytest

Как видим, есть ошибки

-2

Одна ошибка в файле \bnkc\utils\__init__.py. Переменных MONEY_PRECISION и ROUNDING не существует. Их надо удалить.

Другая ошибка в самом тесте. Такое может происходить, когда бизнес логика меняется, а тесты забывают исправить.

В файле \bnkc\tests\test_credit.py строку 77 заменить на

with pytest.raises(ValueError, match="1000"):

Запустим еще раз.

pytest
-3

Тесты пройдены. Теперь запустим тесты покрытия.

pytest --cov=bnkc --cov-report=term-missing
-4

Тесты прошли, но покрытие тестами всего 69%. Считается, что покрывать код 100% тестами не имеет смысла, достаточно 80-90%. У нас сейчас меньше, но это учебный проект, пока достаточно.

Исходники после тестов

- 3a4b45082a9127fb8e3542be4c4159730632bbd8 - pytechnotes/bnkc - Gitverse

Работаем с mypy

Устанавливаем mypy в проект

Заходим в виртуальное окружение и устанавливаем нужную библиотеку

cd bnkc
venv\Scripts\activate
py -m pip install mypy
pip freeze > requirements.txt
deactivate

Запускаем проверку mypy

В корне проекта набираем команду

mypy bnkc

Получаем множество ошибок.

-5

Из ошибок узнаем, что нужна еще одна библиотека.

Заходим в виртуальное окружение и устанавливаем нужную библиотеку

cd bnkc
venv\Scripts\activate
py -m pip install types-python-dateutil
pip freeze > requirements.txt
deactivate

Также надо в файле \bnkc\core\schedule.py заменить строки

total_payment = sum(p.payment for p in self.payments)
total_interest = sum(p.interest for p in self.payments)
total_principal = sum(p.principal for p in self.payments)

на

total_payment = Decimal(sum(p.payment for p in self.payments))
total_interest = Decimal(sum(p.interest for p in self.payments))
total_principal = Decimal(sum(p.principal for p in self.payments))

Набираем команду

mypy bnkc

Ошибок нет

-6

Дополним pyproject.toml

В блок опциональных зависимостей добавляем раздел dev.

dev = [
"mypy>=1.19.1",
"types-python-dateutil>=2.9.0",
]

Создадим раздел с конфигом для mypy. И обновим перечень проверок.

[tool.mypy]
python_version = "3.14"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true

Добавим блок с исключениями

[[tool.mypy.overrides]]
module = [
"tests.*"
]
disallow_untyped_defs = false

Повторно проверяем mypy

Набираем команду

mypy bnkc

Новые ошибки.

-7

Необходимо в файле \bnkc\core\credit.py заменить строку

def from_string(cls, credit_string: str, **kwargs):

на

def from_string(cls, credit_string: str, start_date: str | None = None) -> Credit:

и строку

return cls(amount, rate, term, **kwargs)

на

return cls(amount, rate, term, start_date)

И в файле \bnkc\__main__.py заменить строку

def main():

на

def main() -> None:

Набираем команду

mypy bnkc

Ошибок нет

-8

Исходники после mypy

- a6f5f72a9af2a32187039fa206cae8b791c53a24 - pytechnotes/bnkc - Gitverse

Прочие линтеры

Устанавливаем нужные зависимости в проект

Заходим в виртуальное окружение и устанавливаем нужные библиотеки

cd bnkc
venv\Scripts\activate
py -m pip install isort pylint vulture black
pip freeze > requirements.txt
deactivate

Дополним pyproject.toml

Не будем запускать проверки, давайте сразу укажем нужные настройки и потом запустим.

В блоке опциональных зависимостей обновляем раздел dev.

dev = [
"mypy>=1.19.1",
"types-python-dateutil>=2.9.0",
"black>=26.1.0",
"isort>=7.0.0",
"pylint>=4.0.4",
"vulture>=2.14.0"
]

Добавляем конфиги для pylint

[tool.pylint.master]
fail-under = 8.0
max-line-length = 88
[tool.pylint.messages_control]
disable = [
"missing-module-docstring",
"too-few-public-methods",
"missing-class-docstring",
"missing-function-docstring",
"import-error",
"wrong-import-order",
"broad-exception-caught",
]
[tool.pylint.format]
max-line-length = 88
single-line-if-stmt = true
[tool.pylint.design]
max-attributes = 15
min-public-methods = 1
[tool.pylint.basic]
good-names = ["i", "j", "k", "ex", "run", "a", "b", "x", "y", "args"]

Добавляем конфиги для vulture

[tool.vulture]
exclude = [
"*/tests/*",
"*__pycache__/*",
"*/test_*.py",
"build/*",
"dist/*",
"venv/*",
]
min_confidence = 80

Добавляем конфиги для black

[tool.black]
line-length = 88
target-version = ['py314']

Добавляем конфиги для isort

[tool.isort]
profile = "black"
multi_line_output = 3
line_length = 88

Я расписывать, что означают настройки, не буду. Каждый в своем проекте сам определяет, что ему важно и нужно. Поэтому тут набор настроек может варьироваться.

Запускаем проверки линтеров

Правим стилистику кода

py -m black .

Правим импорты

py -m isort .

Находим ошибки качества кода

py -m pylint bnkc/

Ошибки есть. Надо править. Тут правки описывать не буду. Ниже будет ссылка на репозиторий с правками.

Проверка мёртвого кода

py -m vulture bnkc/

Еще раз прогоняем mypy и тесты

mypy bnkc
pytest

Надеюсь, у вас, как и у меня, всё без ошибок)

Сохраняем проект в репозитории

Отправляем данные в удаленный репозиторий

git add .
git commit -m "пройдена проверка линтерами"
git push -u origin dev

В следующей статье мы настроим CI в GitVerse

Исходники проекта

- 9fa03da1d3b4f88f5f877481f18e719bb958031c - pytechnotes/bnkc - Gitverse

Подписывайтесь на Дзен, а также приглашаю в мой телеграмм канал, там публикую другой, но не менее интересный контент.