Предположим, что вы работаете над сложным проектом с большим количеством модулей. При добавлении новой функциональности нужно обязательно проверить, что система работает как положено, а всё её части отлично согласованы между собой. Для этого необходимо скомпилировать и сконфигурировать не только изменившуюся часть проекта, но и остальные модули, запустить автоматизированные тесты и испытать всю инфраструктуру в режиме, приближенном к боевому.
Простые тесты тут не помогут, поскольку окружения на машинах разработчиков и на production могут различаться. Потребуется целая система сборки и тестирования. Такая система (а вернее принцип или подход) и называется Continuous Integration.
Схема работы с использованием CI
Рассмотрим типовой сценарий, который понимается под фразой “Использовать CI в своём проекте”.
- Разработчик создаёт pull request или commit в репозиторий проекта
- Проект автоматически разворачивается на вспомогательном сервереа. Создаётся пустой контейнер, например с Ubuntuб. Устанавливается необходимый софт для сборки проектав. Клонируется репозиторий с кодом проекта из текущей веткиг. Выполняются скрипты настройки конфигураций
- Происходит запуск Unit-тестов
- Разработчику приходит оповещение о результате
- Если тесты не пройдены, реквест или коммит будет направлен на доработку
Где взять CI
Continuous Integration может быть настроен на вашем сервере с использованием таких инструментов как TeamCity или Jenkins. Однако, для open-source проектов зачастую удобно использовать готовые облачные решения, например, Travis CI или Semaphore CI.
Оба облачных решения позволяют легко сконфигурировать нужное вам окружение, поддерживают множество языков программирования и интегрируются с GitHub.
Как подключить CI
Мы продемонстрируем настройку CI для простейшего веб-приложения с использованием сервиса Semaphore CI. Дополнительно разберёмся с тестированием, оповещением о результатах проверки и интеграцией с GitHub.
Создаём тестовый проект
Напишем веб-приложение, которое будет выводить информацию о погоде в любом городе. Воспользуемся для этого бесплатным погодным API. На сайте достаточно зарегистрироваться и получить токен доступа в личном кабинете. После чего можно формировать запросы вида:
http://api.openweathermap.org/data/2.5/weather?q=Sankt-Peterburg&appid=bf3c0bec12bec9560fde3da3fad53de8
и получать в ответ информацию о погоде в выбранном городе:
{
"coord":{
"lon":30.25,
"lat":59.92
},
"weather":[
{
"id":800,
"main":"Clear",
"description":"clear sky",
"icon":"01n"
}
],
"base":"stations",
"main":{
"temp":260.15,
"pressure":1018,
"humidity":78,
"temp_min":260.15,
"temp_max":260.15
},
"visibility":10000,
"wind":{
"speed":2,
"deg":110
},
"clouds":{
"all":0
},
"dt":1520019000,
"sys":{
"type":1,
"id":7267,
"message":0.0045,
"country":"RU",
"sunrise":1519966347,
"sunset":1520004641
},
"id":536203,
"name":"Sankt-Peterburg",
"cod":200
}
Приведём код приложения на языке Python, которое взаимодействует с данным API.
from flask import Flask
import requests
from flask import request
# Замените своим токеном с openweathermap
APP_ID = 'bf3c0bec12bec9560fde3da3fad53de8'
app = Flask(__name__)
# Извлекает данные о погоде и конвертирует из Кельвина в Цельсий
def parse_weather(data):
try:
temp = data['main']['temp']
return f'The temperature is: {float(temp)-273.15:.1f}°'
except Exception as e:
return f'Data error: {e}'
# Получить данные о погоде через API
def get_weather(city):
try:
r = requests.get(f'http://api.openweathermap.org/data/2.5/weather?q={city}&appid={APP_ID}')
if r.status_code != 200:
return f'Invalid response code: {r.status_code}'
else:
return parse_weather(r.json())
except Exception as e:
return f'API error: {e}'
# Выводит сведения о погоде в выбранном городе
@app.route("/")
def weather():
city = request.args.get('city', 'Sankt-Peterburg')
return get_weather(city)
@app.route("/")
def index():
return "Welcome! The service takes a date and gives corresponding day of the week"
if __name__ == "__main__":
app.run()
Добавляем Unit-тесты
Чтобы автоматически контролировать работоспособность приложения после каждой модификации кода, реализуем следующие тесты:
- API токен задан и имеет длину 32 символа
- Функция main.parse_weather выдаёт ошибку при некорректных данных
- Функция main.parse_weather правильно переводит температуру из Кельвина в Цельсий
- Приложение после взаимодействия с API возвращает код ответа 200
Создайте файл tests.py со следующим содержимым
from flask import request
import main
import unittest
class MainTestCase(unittest.TestCase):
def test_appid_is_set(self):
assert len(main.APP_ID) == 32
def test_parse_error(self):
assert main.parse_weather([]) == 'Data error: list indices must be integers or slices, not str'
def test_convert_temperature(self):
assert main.parse_weather({'main': {'temp': '300'}}) == 'The temperature is: 26.9°'
assert main.parse_weather({'main': {'temp': '273.15'}}) == 'The temperature is: 0.0°'
def test_request(self):
with main.app.test_client() as c:
rv = c.get('/?city=Moscow')
assert rv.status_code == 200
if __name__ == '__main__':
unittest.main()
Для запуска тестов достаточно выполнить следующую команду в директории проекта:
python -m unittest tests.py
Вы получите примерно следующий результат:
....
----------------------------------------------------------------------
Ran 4 tests in 0.144s
OK
Подробнее о тестировании кода вы можете узнать из нашей статьи Автоматизация тестирования на PHP
Настраиваем CI
Разверните репозиторий на GitHub и добавьте туда созданные выше файлы. Пример можно клонировать по ссылке: https://github.com/codex-team/article-ci
Теперь перейдите на сайт https://semaphoreci.com и зарегистрируйте личный аккаунт. Удобно создать аккаунт напрямую через GitHub с помощью кнопки “Get started with GitHub”.
![](https://static.codex.so/upload/editor/o_249889bd3a2ebc976de013e581c4de93.jpg)
Нажмите на ссылку "Create new" для создания нового проекта.
![](https://static.codex.so/upload/editor/o_f2d48a9fe602093fd308f7529afb4b77.jpg)
![](https://static.codex.so/upload/editor/o_0024ee4f49bbc904310436a22c808384.jpg)
![](https://static.codex.so/upload/editor/o_4f7bd49282aa54e5bbe5d462b3fae00c.jpg)
![](https://static.codex.so/upload/editor/o_4560d23c156a20af354092c39e012d6f.jpg)
Платформа Semaphore автоматически подберёт оптимальные настройки, проанализировав ваш репозиторий. Остаётся только отредактировать список команд, которые будут выполнены после каждого коммита или pull-реквеста в репозиторий на GitHub.
Semaphore создаст виртуальный контейнер с Linux, клонирует ваш проект из GitHub и выполнит следующие команды:
- Загрузка необходимых пакетов для Python
pip install -r requirements.txt
- Проверка Unit-тестов
python -m unittest tests.py
![](https://static.codex.so/upload/editor/o_52dc3a875110e213479998e016d8f6fd.jpg)
Проверяем работу
Сделайте любой коммит в основную ветку репозитория на GitHub и перейдите в соответствующий ему проект на Semaphore.
При неуспешном прохождении тестов вы увидите информацию об ошибке сборки и сможете изучить подробный вывод тестов.
![](https://static.codex.so/upload/editor/o_bb1f51ae7472143779375975098b4d7d.jpg)
В случае успешной сборки статус проекта изменится на passed, а в списке коммитов на GitHub вы увидите зелёную галочку.
![](https://static.codex.so/upload/editor/o_389540bb278e85c1e22f45436ed4a701.jpg)
![](https://static.codex.so/upload/editor/o_b36054092c091461d83469ff78089a04.jpg)
Безопасность приватной информации
Чтобы не хранить пароли и ключи доступа к API в открытом виде, их можно поместить в переменные окружения, создаваемые через Semaphore.
Для этого зайдите в настройки проекта и выберите пункт "Environment Variables".
![](https://static.codex.so/upload/editor/o_e500848d0b36a71d15a949e35a35b4a0.jpg)
Замените строчку APP_ID = '...' в скрипте main.py следующим текстом, чтобы брать APP_ID из переменной окружения.
import os
APP_ID = os.environ.get('APP_ID', '')
Получаем оповещения
Semaphore имеет внутренний механизм для выполнения команд в зависимости от результатов тестирования. В случае успеха, переменная окружения SEMAPHORE_THREAD_RESULT примет значение passed, в противном – Failed.
Можно добавить ещё одну команду в настройках проекта на Semaphore, которая будет выполнять действия исходя из значения переменной окружения.
Например, можно посылать результаты проверки напрямую в Telegram-чат через платформу CodeX Bot.
if [ "$SEMAPHORE_THREAD_RESULT" = "passed" ]; then curl --data "message=Success" https://notify.bot.ifmo.su/u/<token>; else curl --data "message=Failed" https://notify.bot.ifmo.su/u/<token>; fi;
![](https://static.codex.so/upload/editor/o_37e3372487b89318a94a31037a1c304e.jpg)
Итоги
Continious Integration позволяет автоматически развернуть и протестировать ваше приложение на системе идентичной рабочему серверу. Для упрощения этого процесса можно воспользоваться универсальными сервисами (например Semaphoreci). Такие сервисы совместимы с большинством современных языков программирования и поддерживают интеграцию с GitHub.
Подробнее о возможностях Semaphoreci – https://semaphoreci.com/docs/
Репозиторий проекта для этой статьи – https://github.com/codex-team/article-ci