Предположим, что вы работаете над сложным проектом с большим количеством модулей. При добавлении новой функциональности нужно обязательно проверить, что система работает как положено, а всё её части отлично согласованы между собой. Для этого необходимо скомпилировать и сконфигурировать не только изменившуюся часть проекта, но и остальные модули, запустить автоматизированные тесты и испытать всю инфраструктуру в режиме, приближенном к боевому.
Простые тесты тут не помогут, поскольку окружения на машинах разработчиков и на 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”.
Нажмите на ссылку "Create new" для создания нового проекта.
Платформа Semaphore автоматически подберёт оптимальные настройки, проанализировав ваш репозиторий. Остаётся только отредактировать список команд, которые будут выполнены после каждого коммита или pull-реквеста в репозиторий на GitHub.
Semaphore создаст виртуальный контейнер с Linux, клонирует ваш проект из GitHub и выполнит следующие команды:
- Загрузка необходимых пакетов для Python
pip install -r requirements.txt
- Проверка Unit-тестов
python -m unittest tests.py
Проверяем работу
Сделайте любой коммит в основную ветку репозитория на GitHub и перейдите в соответствующий ему проект на Semaphore.
При неуспешном прохождении тестов вы увидите информацию об ошибке сборки и сможете изучить подробный вывод тестов.
В случае успешной сборки статус проекта изменится на passed, а в списке коммитов на GitHub вы увидите зелёную галочку.
Безопасность приватной информации
Чтобы не хранить пароли и ключи доступа к API в открытом виде, их можно поместить в переменные окружения, создаваемые через Semaphore.
Для этого зайдите в настройки проекта и выберите пункт "Environment Variables".
Замените строчку 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;
Итоги
Continious Integration позволяет автоматически развернуть и протестировать ваше приложение на системе идентичной рабочему серверу. Для упрощения этого процесса можно воспользоваться универсальными сервисами (например Semaphoreci). Такие сервисы совместимы с большинством современных языков программирования и поддерживают интеграцию с GitHub.
Подробнее о возможностях Semaphoreci – https://semaphoreci.com/docs/
Репозиторий проекта для этой статьи – https://github.com/codex-team/article-ci