Довгий час написання тестів здавалося мені чимось для «суворих ентерпрайз-розробників». Але це стає справді важливим під час програмування за допомогою ШІ, особливо за доволі масштабних змін, коли доводиться тестувати буквально все, щоб переконатися, що нічого не зламалося.
У мене був робочий пет-проєкт — утиліта на 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% дало мені цілковиту впевненість у тому, що основа програми надійна, як швейцарський годинник.
Агенти
Звісно, усе було зовсім не так) Було швидше й простіше — інакше, без ШІ та агента, я витратив би багато днів.
Звісно, агент може написати тести, навіть якщо ви взагалі нічого не розумітимете й не перевірятимете за ним. Але я використовував його для швидкого старту: просив розповісти про тести, він показував приклади й пояснював концепції, я розпитував про різні технічні моменти — як краще робити та звідки беруться дані про покриття коду тестами. Усе це виявилося не лише цікавим, а й корисним для кращого розуміння, що тестувати і як. Звісно, я не можу сказати, що колись писатиму їх вручну: усе-таки це повільна рутина. А агенти зрештою для цього й потрібні — робити те, що з їхньою допомогою можна зробити добре та швидко.
