Долгое время написание тестов казалось мне чем-то для «суровых энтерпрайз-разработчиков». Но это становится действительно важно при программировании при помощи ИИ, особенно при довольно обширных правках, когда приходится тестировать буквально все, чтобы убедится что ничего не сломано.
У меня был работающий пет-проект — утилита на Python для подписи картинок и при попытке его рефакторинга сломалось очень многое, все функции пришлось тестировать по новой и не зря. Так вот чтобы знать где и что отвалилось хорошо бы иметь тесты.
Страх №1: «Придется переписывать часть кода проекта»
Мой первый и самый главный страх: чтобы внедрить тесты, мне придется как то менять рабочий код под какую-нибудь «тестопригодную архитектуру».
Реальность: Ничего менять не пришлось! Я просто создал рядышком папку tests и положил туда пустой файлик init.py (чтобы Python понял, что это пакет), а также свои первые тестовые скрипты: test_db.py и test_main.py.
Все, что мне понадобилось установить — это пару пакетов:
pip install pytest pytest-cov
pytest сам находит все файлы, начинающиеся на test_, и запускает их.
Моки (Mock) и Фикстуры (Fixture): Как тестировать интерфейс без самого интерфейса
Главная сложность моего проекта — графический интерфейс (GUI) на Tkinter. Как тестировать нажатие кнопок, если окно зависает и ждет действий пользователя? А если выскакивает окошко с ошибкой messagebox.showerror — тест останавливается?
Оказалось, в Python есть гениальная вещь — unittest.mock. Она позволяет создавать «шпионов» и подменять реальные функции на их имитацию.
Например, чтобы тесты не блокировались всплывающими окнами, я просто «замокал» их:
from unittest.mock import patch
with patch('main.messagebox.showinfo'):
# Теперь, если программа вызовет showinfo,
# окно не появится, но тест зафиксирует, что вызов был!
app.do_something()
А чтобы каждый раз не создавать окно Tkinter с нуля (что вызывает ошибки на Windows, если делать это десятки раз за секунду), я использовал фикстуры. Фикстура — это как бы «подготовительная база» для теста. Я создал одно окно Tkinter, скрыл его (root.withdraw()), и все тесты спокойно работали с этим скрытым окном в фоновом режиме.
Тестирование работы с файлами: tmp_path нас спасет
Моя программа переименовывает картинки и сохраняет requirements.txt файлы. Я очень боялся, что тесты начнут создавать мусор на моем жестком диске или, чего доброго, удалят настоящие фотографии.
Здесь pytest дарит нам магическую встроенную фикстуру tmp_path. Она автоматически создает временную папку для каждого теста и сама удаляет её после окончания!
В итоге тест переименования файла выглядит так: мы создаем виртуальный screen.png файл во временной папке, передаем его программе, дергаем функцию main.py#rename_file() и проверяем:
- Исчез ли старый файл?
- Появился ли новый файл?
- Обновилось ли имя в базе данных?
Всё это делается простейшими assert:
assert (tmp_path / "new_img1.png").exists() # Новый файл должен появиться!
Как протестировать то, чего нет на экране? (Крадем функции)
Самое интересное было, когда я захотел протестировать функцию массового поиска и замены текста. Окно поиска всплывает интерактивно, внутри него создается кнопка, а к кнопке привязана внутренняя функция main.py#perform_replace().
С помощью моков я смог перехватить сам процесс создания кнопки интерфейса, «вытащить» из неё эту функцию main.py#perform_replace и запустить её напрямую прямо в тесте, подсунув ей фейковые значения в поля ввода! Это вызвало настоящий восторг — я проверил работу интерфейса, ни разу не отрисовав его на экране.
«Покрытие кода» (Coverage)
Когда все тесты позеленели, встал вопрос — а всё ли я протестировал? Тут на сцену выходит плагин pytest-cov.
Я запустил команду:
pytest --cov=main --cov=db --cov-report=html tests/
Она сгенерировала HTML-страничку, в которой мой рабочий код раскрашен в цвета. Зеленая строчка — тест сюда дошел. Красная строчка — здесь тест не бывал.
Поначалу я захотел 100% покрытия. Но быстро понял, что красными остаются очень специфические куски кода: например, except OSError (когда файл физически заблокирован другим процессом). Имитировать такие системные сбои в тестах можно, но для небольшого проекта это излишне. Покрытие в 80-90% дало мне абсолютную уверенность в том, что основа программы надежна, как швейцарские часы.
Агенты
Конечно все было совсем не так) Было быстрей и проще вместо того чтобы потратить много дней, если бы я не использовал ИИ и агента.
Конечно агент может написать тесты, даже если вы вообще не будете понимать ничего и проверять после него, но я его использовал для быстрого старта — просил рассказать о тестах, он показывал примеры и рассказывал концепции, я расспрашивал разные технические моменты — как лучше и откуда берутся данные о покрытии кода тестами. Это все оказалось не только интересно но и полезно для лучше понимания что тестировать и как. Конечно я не могу сказать что буду когда либо их писать вручную, все же это медленная рутина, а агенты в конце концов и нужны для этого, чтоб делать то что может быть сделано хорошо и быстро при помощи их.
