Главная страница
Навигация по странице:

  • PATCH /imports/$import_id/citizens/$citizen_id

  • GET /imports/$import_id/citizens/birthdays

  • GET /imports/$import_id/towns/stat/percentile/age

  • Подменяем базовую дату в обработчике на время теста from unittest.mock import patch import

  • Например, вот так import pytz from analyzer.utils.testing import

  • Полное описание workflow

  • курсовая_v1. Курсовая работа Необходимо


    Скачать 2.93 Mb.
    НазваниеКурсовая работа Необходимо
    Дата30.03.2022
    Размер2.93 Mb.
    Формат файлаpdf
    Имя файлакурсовая_v1.pdf
    ТипКурсовая
    #429949
    страница5 из 6
    1   2   3   4   5   6

    Данные с ошибками, ожидаем HTTP-ответ 400: Bad Request.

    Дата рождения некорректная (будущее время).
    • citizen_id в рамках выгрузки не уникален.

    Родственная связь указана неверно (есть только от одного жителя к другому, но нет обратной).

    У жителя указан несуществующий в выгрузке родственник.

    Родственные связи не уникальны.
    Если обработчик отработал успешно и данные были добавлены, необходимо получить добавленных в БД жителей и сравнить их с эталонной выгрузки. Для получения жителей я воспользовался уже протестированным обработчиком
    GET
    /imports/$import_id/citizens
    , а для сравнения — функцией compare_citizen_groups
    PATCH /imports/$import_id/citizens/$citizen_id
    Валидация данных во многом похожа на описанную в обработчике
    POST /imports с небольшими исключениями: есть только один житель и клиент может передать только те поля, которые пожелает
    Я решил использовать следующие наборы с некорректными данными, чтобы проверить, что обработчик вернет HTTP-ответ
    400: Bad request
    :

    Поле указано, но имеет некорректный тип и/или формат данных

    Указана некорректная дата рождения (будущее время).

    Поле relatives содержит несуществующего в выгрузке родственника.
    Также необходимо проверить, что обработчик корректно обновляет информацию о жителе и его родственниках.
    Для этого создадим выгрузку с тремя жителями, два из которых — родственники, и отправим запрос с новыми значениями всех скалярных полей и новым идентификатором родственника в поле relatives
    Чтобы убедиться, что обработчик различает жителей разных выгрузок перед тестом (и, например, не изменит жителей с одинаковыми идентификаторами из другой выгрузки), я создал дополнительную выгрузку с тремя жителями, которые имеют такие же идентификаторы.
    Обработчик должен сохранить новые значения скалярных полей, добавить нового указанного родственника и удалить связь со старым, не указанным
    родственником. Все изменения родственных связей должны быть двусторонними.
    Изменений в других выгрузках быть не должно.
    Поскольку такой обработчик может быть подвержен состоянию гонки (это рассматривалось в разделе «Разработка»), я добавил два дополнительных теста
    Один воспроизводит проблему с состоянием гонки (расширяет класс обработчика и убирает блокировку), второй доказывает, что проблема с состоянием гонки не воспроизводится.
    GET /imports/$import_id/citizens/birthdays
    Для тестирования этого обработчика я выбрал следующие наборы данных:

    Выгрузка, в которой у жителя есть один родственник в одном месяце и два родственника в другом.

    Выгрузка с одним жителем без родственников. Проверяет, что обработчик не учитывает его при расчетах.

    Пустая выгрузка. Проверяет, что обработчик не упадет с ошибкой и вернет в ответе корректный словарь с 12 месяцами.

    Выгрузка с жителем, который сам себе родственник. Проверяет, что житель купит себе подарок в месяц своего рождения.
    Обработчик должен возвращать в ответе все месяцы, даже если в эти месяцы нет дней рождений. Чтобы избежать дублирования, я сделал функцию, которой можно передать словарь, чтобы она дополнила его значениями для отсутствующих месяцев.
    Чтобы убедиться, что обработчик различает жителей разных выгрузок, я добавил дополнительную выгрузку с двумя родственниками. Если обработчик по ошибке использует их при расчетах, то результаты будут некорректными и обработчик упадет с ошибкой.
    GET /imports/$import_id/towns/stat/percentile/age
    Особенность этого теста в том, что результаты его работы зависят от текущего времени: возраст жителей вычисляется исходя из текущей даты. Чтобы результаты тестирования не менялись с течением времени, текущую дату, даты рождения жителей и ожидаемые результаты необходимо зафиксировать. Это позволит легко воспроизвести любые, даже краевые случаи.
    Как лучше зафиксировать дату? В обработчике для вычисления возраста жителей используется PostgreSQL-функция
    AGE
    , принимающая первым параметром дату, для которой необходимо рассчитать возраст, а вторым — базовую дату
    (определена константой
    TownAgeStatView.CURRENT_DATE
    ).
    Подменяем базовую дату в обработчике на время теста
    from
    unittest.mock
    import
    patch

    import
    pytz
    CURRENT_DATE = datetime(
    2020
    ,
    2
    ,
    17
    , tzinfo=pytz.utc)
    @patch('analyzer.api.handlers.TownAgeStatView.CURRENT_DATE', new=CURRENT_DATE)
    async
    def
    test_get_ages
    (...)
    :
    Для тестирования обработчика я выбрал следующие наборы данных (для всех жителей указывал один город, потому что обработчик агрегирует результаты по городам):

    Выгрузка с несколькими жителями, у которых завтра день рождения
    (возраст — несколько лет и 364 дня). Проверяет, что обработчик использует в расчетах только количество полных лет.

    Выгрузка с жителем, у которого сегодня день рождения (возраст — ровно несколько лет). Проверяет краевой случай — возраст жителя, у которого сегодня день рождения, не должен рассчитаться как уменьшенный на 1 год.

    Пустая выгрузка. Обработчик не должен на ней падать.
    Эталон для расчета перцентилей — numpy с линейной интерполяцией, и эталонные результаты для тестирования я рассчитал именно им.
    Также нужно округлять дробные значения перцентилей до двух знаков после запятой. Если вы использовали в обработчике для округления PostgreSQL, а для расчета эталонных данных — Python, то могли заметить, что округление в
    Python 3 и PostgreSQL может давать разные результаты.
    Например
    # Python 3 round(2.5)
    > 2
    -- PostgreSQL
    SELECT ROUND(2.5)
    > 3
    Дело в том, что Python использует банковское округление до ближайшего четного
    , а PostgreSQL — математическое (half-up). В случае, если расчеты и округление производятся в PostgreSQL, было бы правильным в тестах также использовать математическое округление.
    Сначала опишите наборы данных с датами рождения в текстовом формате, но читать тест в таком формате будет неудобно: придется каждый раз вычислять в уме возраст каждого жителя, чтобы вспомнить, что проверяет тот или иной набор данных. Конечно, можно было обойтись комментариями в коде, но я решил пойти чуть дальше и написал функцию age2date
    , которая позволяет описать дату
    рождения в виде возраста: количества лет и дней.
    Например, вот так
    import
    pytz
    from
    analyzer.utils.testing
    import
    generate_citizen
    CURRENT_DATE = datetime(
    2020
    ,
    2
    ,
    17
    , tzinfo=pytz.utc)
    def
    age2date
    (years: int, days: int = 0, base_date=CURRENT_DATE)
    -> str: birth_date = copy(base_date).replace(year=base_date.year - years) birth_date -= timedelta(days=days)
    return
    birth_date.strftime(BIRTH_DATE_FORMAT)
    # Сколько лет этому жителю? Посчитать несложно, но если их будет много?
    generate_citizen(birth_date=
    '17.02.2009'
    )
    # Жителю ровно 11 лет и у него сегодня день рождения generate_citizen(birth_date=age2date(years=
    11
    ))
    Чтобы убедиться, что обработчик различает жителей разных выгрузок, я добавил дополнительную выгрузку с одним жителем из другого города: если обработчик по ошибке использует его, в результатах появится лишний город и тест сломается.
    Миграции
    Код миграций на первый взгляд кажется очевидным и наименее подверженным ошибкам, зачем его тестировать? Это очень опасное заблуждение: самые коварные ошибки миграций могут проявить себя в самый неподходящий момент.
    Даже если они не испортят данные, то могут стать причиной лишнего даунтайма.
    Существующая в проекте initial миграция изменяет структуру базы данных, но не изменяет данные. От каких типовых ошибок можно защититься в подобных миграциях?

    Метод downgrade не реализован или не удалены все созданные в миграции сущности (особенно это касается пользовательских типов данных, которые создаются автоматически при создании таблицы, я про них уже упоминал).
    Это приведет к тому, что миграцию нельзя будет применить два раза
    (применить-откатить-применить): при откате не будут удалены все созданные миграцией сущности, при повторном создании миграция пройдет с ошибкой — тип данных уже существует.

    Cинтаксические ошибки и опечатки.

    Ошибки в связях миграций (цепочка нарушена).
    Большинство этих ошибок обнаружит stairway-тест
    . Его идея — применять миграции по одной, последовательно выполняя методы upgrade
    , downgrade
    , upgrade для каждой миграции. Такой тест достаточно
    один раз добавить в проект, он не требует поддержки и будет служить верой и правдой.
    А вот если миграция, помимо структуры, изменяла бы данные, то потребовалось бы написать хотя бы один отдельный тест, проверяющий, что данные корректно изменяются в методе upgrade и возвращаются к изначальному состоянию в downgrade
    . На всякий случай: проект с примерами тестирования разных миграций
    ,
    Сборка
    Конечный артефакт, который мы собираемся разворачивать и который хотим получить в результате сборки, — Docker-образ. Для сборки необходимо выбрать
    базовый образ c Python. Официальный образ python:latest весит

    1 ГБ и, если его использовать в качестве базового, образ с приложением будет огромным. Существуют образы на основе ОС Alpine
    , размер которых намного меньше. Но с растущим количеством устанавливаемых пакетов размер конечного образа вырастет, и в итоге даже образ, собранный на основе Alpine, будет не таким уж и маленьким. Я выбрал в качестве базового образа snakepacker/python
    — он весит немного больше Alpine-образов, но основан на Ubuntu, которая предлагает огромный выбор пакетов и библиотек.
    Еще один способ уменьшить размер образа с приложением — не включать в итоговый образ компилятор, библиотеки и файлы с заголовками для сборки, которые не потребуются для работы приложения.
    Для этого можно воспользоваться многоступенчатой сборкой
    Docker:
    1. С помощью «тяжелого» образа snakepacker/python:all (1 ГБ, в сжатом виде 500 МБ) создаем виртуальное окружение, устанавливаем в него все зависимости и пакет с приложением. Этот образ нужен исключительно для сборки, он может содержать компилятор, все необходимые библиотеки и файлы с заголовками.
    2. FROM snakepacker/python:all
    as
    builder
    3.
    4.
    # Создаем виртуальное окружение
    5. RUN python3
    .8
    -m venv /usr/share/python3/app
    6.
    7.
    # Копируем source distribution в контейнер и устанавливаем его
    8. COPY dist/ /mnt/dist/
    RUN /usr/share/python3/app/bin/pip install /mnt/dist/*
    9. Готовое виртуальное окружение копируем в «легкий» образ snakepacker/python:3.8 (100 МБ, в сжатом виде 50 МБ), который содержит только интерпретатор требуемой версии Python.

    Важно: в виртуальном окружении используются абсолютные пути, поэтому его необходимо скопировать по тому же адресу, по которому оно было собрано в контейнере-сборщике.
    10. FROM snakepacker/python:
    3.8
    as
    api
    11.
    12.
    # Копируем готовое виртуальное окружение из контейнера builder
    13. COPY --
    from
    =builder /usr/share/python3/app /usr/share/python3/app
    14.
    15.
    # Устанавливаем ссылки, чтобы можно было воспользоваться командами
    16.
    # приложения
    17. RUN ln -snf /usr/share/python3/app/bin/analyzer-* /usr/local/bin/
    18.
    19.
    # Устанавливаем выполняемую при запуске контейнера команду по умолчанию
    CMD [
    "analyzer-api"
    ]
    Чтобы сократить время на сборку образа, зависимые модули приложения можно установить до его установки в виртуальное окружение. Тогда Docker закеширует их и не будет устанавливать заново, если они не менялись.
    Dockerfile целиком
    ############### Образ для сборки виртуального окружения ################
    # Основа — «тяжелый» (1 ГБ, в сжатом виде 500 ГБ) образ со всеми необходимыми
    # библиотеками для сборки модулей
    FROM snakepacker/python:all
    as
    builder
    # Создаем виртуальное окружение и обновляем pip
    RUN python3
    .8
    -m venv /usr/share/python3/app
    RUN /usr/share/python3/app/bin/pip install -U pip
    # Устанавливаем зависимости отдельно, чтобы закешировать. При последующей сборке
    # Docker пропустит этот шаг, если requirements.txt не изменится
    COPY requirements.txt /mnt/
    RUN /usr/share/python3/app/bin/pip install -Ur /mnt/requirements.txt
    # Копируем source distribution в контейнер и устанавливаем его
    COPY dist/ /mnt/dist/
    RUN /usr/share/python3/app/bin/pip install /mnt/dist/* \
    && /usr/share/python3/app/bin/pip check
    ########################### Финальный образ ############################
    # За основу берем «легкий» (100 МБ, в сжатом виде 50 МБ) образ с Python
    FROM snakepacker/python:
    3.8
    as
    api
    # Копируем в него готовое виртуальное окружение из контейнера builder
    COPY --
    from
    =builder /usr/share/python3/app /usr/share/python3/app
    # Устанавливаем ссылки, чтобы можно было воспользоваться командами
    # приложения
    RUN ln -snf /usr/share/python3/app/bin/analyzer-* /usr/local/bin/

    # Устанавливаем выполняемую при запуске контейнера команду по умолчанию
    CMD [
    "analyzer-api"
    ]
    Для удобства сборки я добавил команду make upload
    , которая собирает Docker- образ и загружает его на hub.docker.com.
    CI
    Теперь, когда код покрыт тестами и мы умеем собирать Docker-образ, самое время автоматизировать эти процессы. Первое, что приходит в голову: запускать тесты на создание пул-реквестов, а при добавлении изменений в master-ветку собирать новый Docker-образ и загружать его на
    Docker Hub
    (или
    GitHub
    Packages
    , если вы не собираетесь распространять образ публично).
    Можно решить эту задачу с помощью
    GitHub Actions
    . Для этого потребовалось создать YAML-файл в папке
    .github/workflows и описать в нем workflow (c двумя задачами: test и publish
    ), которое я назвал
    CI
    Задача test выполняется при каждом запуске workflow
    CI
    , с помощью services поднимает контейнер с PostgreSQL, ожидает, когда он станет доступен, и запускает pytest в контейнере snakepacker/python:all
    Задача publish выполняется, только если изменения были добавлены в ветку master и если задача test была выполнена успешно. Она собирает source distribution контейнером snakepacker/python:all
    , затем собирает и загружает
    Docker-образ с помощью docker/build-push-action@v1
    Полное описание workflow
    name: CI
    # Workflow должен выполняться при добавлении изменений
    # или новом пул-реквесте в master on: push: branches: [ master ] pull_request: branches: [ master ] jobs:
    # Тесты должны выполняться при каждом запуске workflow test: runs-on: ubuntu-latest services: postgres: image: docker://postgres ports:
    - 5432:5432 env:
    POSTGRES_USER: user
    POSTGRES_PASSWORD: hackme

    POSTGRES_DB: analyzer steps:
    - uses: actions/checkout@v2
    - name: test uses: docker://snakepacker/python:all env:
    CI_ANALYZER_PG_URL: postgresql://user:hackme@postgres/analyzer with: args: /bin/bash -c "pip install -U '.[dev]' && pylama && wait-for- port postgres:5432 && pytest -vv --cov=analyzer --cov-report=term-missing tests"
    # Сборка и загрузка Docker-образа с приложением publish:
    # Выполняется только если изменения попали в ветку master if: github.event_name == 'push' && github.ref == 'refs/heads/master'
    # Требует, чтобы задача test была выполнена успешно needs: test runs-on: ubuntu-latest steps:
    - uses: actions/checkout@v2
    - name: sdist uses: docker://snakepacker/python:all with: args: make sdist
    - name: build-push uses: docker/build-push-action@v1 with: username: ${{ secrets.REGISTRY_LOGIN }} password: ${{ secrets.REGISTRY_TOKEN }} repository: alvassin/backendschool2019 target: api tags: 0.0.1, latest
    Теперь при добавлении изменений в master во вкладке Actions на GitHub можно увидеть запуск тестов, сборку и загрузку Docker-образа:
    А при создании пул-реквеста в master-ветку в нем также будут отображаться
    результаты выполнения задачи test
    :
    Деплой
    Чтобы развернуть приложение на предоставленном сервере, нужно установить
    Docker, Docker Compose, запустить контейнеры с приложением и PostgreSQL и применить миграции.
    Эти шаги можно автоматизировать с помощью системы управления конфигурациями Ansible. Она написана на Python, не требует специальных агентов (подключается прямо по ssh), использует jinja-шаблоны и позволяет декларативно описывать желаемое состояние в YAML-файлах. Декларативный подход позволяет не задумываться о текущем состоянии системы и действиях, необходимых, чтобы привести систему к желаемому состоянию. Вся эта работа ложится на плечи модулей Ansible.
    Ansible позволяет сгруппировать логически связанные задачи в роли и затем переиспользовать. Нам потребуются две роли: docker
    (устанавливает и настраивает Docker) и analyzer
    (устанавливает и настраивает приложение).
    Роль
    docker
    добавляет в систему репозиторий с Docker, устанавливает и настраивает пакеты docker-ce и docker-compose
    Опционально можно наладить автоматическое возобновление работы REST API после перезагрузки сервера. Ubuntu позволяет решить эту задачу силами системы инициализации systemd
    . Она управляет юнитами, представляющими собой различные ресурсы (демоны, сокеты, точки монтирования и другие). Чтобы добавить новый юнит в systemd, необходимо описать его конфигурацию в отдельном файле .service и разместить этот файл в одной из специальных папок, например в
    /etc/systemd/system
    . Затем юнит можно запустить, а также включить для него автозагрузку.
    Пакет docker-ce при установке автоматически создаст файл с конфигурацией юнита — необходимо только убедиться, что он запущен и включается при запуске системы. Для Docker Compose файл конфигурации docker-compose@.service будет
    создан силами Ansible. Символ
    @
    в названии указывает systemd, что юнит является шаблоном. Это позволяет запускать сервис docker-compose с параметром — например, с названием нашего сервиса, который будет подставлен вместо
    %i в файле конфигурации юнита:
    [Unit]
    Description=%i service with docker compose
    Requires=docker.service
    After=docker.service
    [Service]
    Type=oneshot
    RemainAfterExit=true
    WorkingDirectory=/etc/docker/compose/%i
    ExecStart=/usr/local/bin/docker-compose up -d --remove-orphans
    ExecStop=/usr/local/bin/docker-compose down
    [Install]
    WantedBy=multi-user.target
    1   2   3   4   5   6


    написать администратору сайта