Інтро
У статті розберемося, як організувати процес неперервної інтеграції (Continuous Integration, CI) на GitLab для ваших Python-застосунків. Для наочного прикладу використаємо реальний застосунок.

Після прочитання цієї статті ви матимете уявлення про те, як за допомогою CI виконати такі завдання для застосунків на Python:
- модульне та функціональне тестування з pytest;
- лінтинг за допомогою flake8;
- статичний аналіз з pylint;
- перевірка типів з mypy.
Що таке CI
Неперервна інтеграція передбачає постійне тестування робочих копій застосунку. Термін «тестування» в цьому контексті може означати:
- інтеграційне тестування;
- модульне тестування;
- функціональне тестування;
- статичний аналіз;
- перевірку стилю (лінтинг);
- динамічний аналіз.
Аби максимально полегшити виконання усього розмаїття тестів, краще запускати їх автоматично як частину управління конфігураціями (git). Тут на допомогу і приходить GitLab CI!
Початок роботи з GitLab CI
Перед тим як перейти безпосередньо до GitLab CI, оглянемо деякі основні терміни:
- pipeline — набір тестів, які запускаються після одного git-коміту;
- runner — сервер, який виконує тести при кожному коміті. GitLab передбачає власні runners, проте ви також можете використовувати свої сервери;
- job — один тест, запущений у pipeline;
- stage — група пов'язаних між собою тестів, запущених у pipeline.
Це зображення допоможе краще розібратися в наведених поняттях:

GitLab використовує файл .gitlab-ci.yml
для запуску CI pipeline у проекті. Цей файл слід шукати в директорії верхнього рівня.
Існує багато способів запустити тест в GitLab CI, проте рекомендується запускати тести в окремих контейнерах Docker. Насправді, час на запуск контейнера у порівнянні з загальним часом виконання тестів у CI дуже незначний.
Створюємо Job в GitLab CI
Перший job, який ми додамо до проекту — запуск лінтера (flake8). Для локального середовища розробки виконаємо таку команду:
$ flake8 --max-line-length=120 bild/*.py
Щоб перетворити цю команду на job у GitLab CI, треба внести до файлу .gitlab-ci.yml
такі зміни:
image: "python:3.7"
before_script:
- python --version
- pip install -r requirements.txt
stages:
- Static Analysis
flake8:
stage: Static Analysis
script:
- flake8 --max-line-length=120 bild/*.py
Цей yaml-файл описує, що саме GitLab CI повинен запустити при кожному коміті, відправленому до репозиторію. Розберемо його крок за кроком.
У першому рядку (image: "python: 3.7"
) ми вказуємо GitLab CI використовувати Docker для виконання усіх тестів для цього проекту, зокрема використовувати для цього образ python:3.7
, розташований на DockerHub.
Наступна частина before_script
— набір команд, які запускаються в контейнері Docker перед початком кожного job. Дійсно зручно отримувати вже налаштований контейнер Docker, встановивши усі необхідні застосунку пакети Python.
Далі переходимо до stages
, де визначено різні етапи в pipeline. Поки там є лише статичний аналіз, але трохи пізніше ми додамо наступний етап — тест. Етапи можна вважати зручним способом групування пов'язаних між собою job.
І наостанок: четверта частина (flake8
) визначає сам job. Спочатку вказується етап (у нас це статичний аналіз), частиною якого буде job, а також команди, що будуть запущені в контейнері Docker для визначеного job. В нашому прикладі для Python-файлів застосунку буде запускатися лінтер flake8.
На цьому етапі зміни файлу .gitlab-ci.yml
треба закомітити та запушити в GitLab:
git add .gitlab_ci.yml
git commit -m "Updated .gitlab_ci.yml"
git push origin master
GitLab CI визначить конфігураційний файл та використає його для запуску pipeline:

Тож ми розібралися, як запустити CI процес для проекту на Python. Перейдемо до тестів.
Запуск тестів з pytest на GitLab CI
Для запуску модульних та функціональних тестів з pytest у середовищі розробки слід виконати таку команду в директорії верхнього рівня:
$ pytest
Перша спроба створити новий job для запуску pytest у файлі .gitlab-ci.yml
мала такий вигляд:
image: "python:3.7"
before_script:
- python --version
- pip install -r requirements.txt
stages:
- Static Analysis
- Test
...
pytest:
stage: Test
script:
- pytest
Однак це не спрацювало, оскільки pytest не міг знайти модуль bild
(тобто сирцевий код) для тесту:

$ pytest
========================= test session starts ==========================
platform linux -- Python 3.7.3, pytest-4.5.0, py-1.5.4, pluggy-0.11.0
rootdir: /builds/patkennedy79/bild, inifile: pytest.ini
plugins: datafiles-2.0
collected 0 items / 3 errors
============================ ERRORS ====================================
___________ ERROR collecting tests/functional/test_bild.py _____________
ImportError while importing test module '/builds/patkennedy79/bild/tests/functional/test_bild.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
tests/functional/test_bild.py:4: in <module>
from bild.directory import Directory
E ModuleNotFoundError: No module named 'bild'
...
==================== 3 error in 0.24 seconds ======================
ERROR: Job failed: exit code 1
Проблема полягала в тому, що модуль bild
був недоступний для файлів test_*.py
, тому що директорія верхнього рівня в проекті не була визначена в системному шляху:
$ python -c "import sys;print(sys.path)"
['', '/usr/local/lib/python37.zip', '/usr/local/lib/python3.7', '/usr/local/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/site-packages']
Розв'язати таку проблему можна, додавши директорію верхнього рівня до системного шляху всередині контейнера Docker потрібного job:
pytest:
stage: Test
script:
- pwd
- ls -l
- export PYTHONPATH="$PYTHONPATH:."
- python -c "import sys;print(sys.path)"
- pytest
Тепер, як бачимо, job може успішно виконуватися:
$ pwd
/builds/patkennedy79/bild
$ export PYTHONPATH="$PYTHONPATH:."
$ python -c "import sys;print(sys.path)"
['', '/builds/patkennedy79/bild', '/usr/local/lib/python37.zip', '/usr/local/lib/python3.7', '/usr/local/lib/python3.7/lib-dynload', '/usr/local/lib/python3.7/site-packages']

Остаточна конфігурація GitLab CI
Такий вигляд матиме остаточний файл .gitlab-ci.yml
, що запускає jobs статичного аналізу (flake8, mypy, pylint) та тести (pytest):
image: "python:3.7"
before_script:
- python --version
- pip install -r requirements.txt
stages:
- Static Analysis
- Test
mypy:
stage: Static Analysis
script:
- pwd
- ls -l
- python -m mypy bild/file.py
- python -m mypy bild/directory.py
flake8:
stage: Static Analysis
script:
- flake8 --max-line-length=120 bild/*.py
pylint:
stage: Static Analysis
allow_failure: true
script:
- pylint -d C0301 bild/*.py
unit_test:
stage: Test
script:
- pwd
- ls -l
- export PYTHONPATH="$PYTHONPATH:."
- python -c "import sys;print(sys.path)"
- pytest
Результат з GitLab CI:

Зверніть увагу, що pylint висуває деякі попередження для нормальної роботи; їх можна позбутися, налаштувавши allow_failure
:
pylint:
stage: Static Analysis
allow_failure: true
script:
- pylint -d C0301 bild/*.py
Ще немає коментарів