В этой заключительной части серии мы напишем модульные тесты для нашего Flask API. К этому моменту мы уже изучили, как написать простой Flask API, настроить его внутри докера и использовать Flask Blueprints, чтобы сделать его модульным.

В конце этой части наша структура папок будет выглядеть так:

flask-ml-api
|- api
    |- __init__.py
    |- endpoints
        |- __init__.py
        |- classification.py
    |- models
        |- model.pkl
    |- tests
        |- __init__.py
        |- test_classification.py
    |- app.py
    |- wsgi.py
    |- requirements.txt
    |- Dockerfile
|- nginx
    |- nginx.conf
    |- Dockerfile
|- docker-compose.yml
|- run_unittests.py

Шаг 1. Написание тестов для каждой конечной точки

В нашем API есть только одна конечная точка, которая находится в classification.py. Однако у вас может быть больше конечных точек, если вы сделали это в Части 3.

Начнем с создания нового каталога для всех наших тестов. В этом каталоге я создаю новый файл для каждой конечной точки. В данном случае у меня tests/test_classification.py

Мы будем использовать unitest библиотеку для написания и выполнения тестов.

Вот как выглядит мой test_classification.py:

import unittest
import json
from flask import Flask
from api.endpoints.classification import classification_api

app = Flask(__name__)
app.register_blueprint(classification_api)


class ClassificationTests(unittest.TestCase):

    tester = None

    def __init__(self, *args, **kwargs):
        super(ClassificationTests, self).__init__(*args, **kwargs)
        global tester
        tester = app.test_client()

    def test_classify_single(self):
        response = tester.get(
            '/classification',
            data=json.dumps({"text": "Cocoa setup issues"}),
            content_type='application/json'
        )

        data = response.get_data(as_text=True)
        print("Category predicted: "+str(data))
        self.assertEqual(response.status_code, 200)
        self.assertIsNotNone(data)


if __name__ == '__main__':
    unittest.main()

Во-первых, сам тест - это приложение Flask. Это необходимо, потому что наши конечные точки существуют как Flask Blueprints, как описано в части 3 этой серии.

  • Мы вызываем register_blueprint() и передаем план classification_api, который мы создали в classification.py
  • После этого мы сохраним app.test_client() в локальной переменной тестера. Это даст нам доступ к API, как будто мы попадаем в него с реальным трафиком.
  • Обратите внимание, что в методе __init__ я должен был убедиться, что тестер является глобальной переменной.

Теперь вы можете написать тестовую функцию, в моем случае это test_classify_single. Здесь мы можем использовать tester.get() для выполнения запроса GET на конечной точке '/classification'. data и content_type зависят от типа запроса, который может обработать ваш API. В этом случае я передаю тестовый JSON:

{"text": "Cocoa setup issues"}

Это JSON, который мой API должен уметь декодировать, передавать в модель, которая его классифицирует, а затем отправлять обратно категорию как JSON.

  • Доступ к этому возвращенному JSON можно получить с помощью метода response.get_data() в виде текста, который я затем могу распечатать или записать в утверждения утверждения.
  • Мои утверждения assert проверяются, чтобы убедиться, что возвращенный код состояния является успешным (который должен быть 200) для этой структуры входного JSON, и что данные в ответе не None (т.е. null).

ВАЖНОЕ ПРИМЕЧАНИЕ. При использовании ML API тестировать результат прогноза - плохая практика, поскольку обновление модели может привести к сбою тестов, даже если API работает нормально. Такое тестирование должно происходить на этапе проверки разработки вашей модели, а не после внедрения API. Например, проверка того, является ли оператор типа “This is a furry feline that purrs” предсказанием “cat” класса плохим.

Вместо этого сконцентрируйте усилия по тестированию на том, чтобы убедиться, что ваш API может обрабатывать странный ввод JSON, недопустимый ввод и предоставлять правильные коды ответа.

Шаг 2. Напишите сценарий для запуска всех тестов для всех конечных точек.

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

Мой файл сценария называется run_unittests.py и находится в домашней папке проекта (вместе с docker-compose - см. Структуру файлов выше)

Мой run_unittests.py выглядит так:

import unittest

print("Running unit tests from api/tests directory...")

loader = unittest.TestLoader()
start_dir = 'api/tests'
suite = loader.discover(start_dir)

runner = unittest.TextTestRunner()
runner.run(suite)

print("Running tests is complete")

Все это создает TestLoader, который загружает все тестовые классы из папки 'api/tests' и запускает весь пакет.

Этот код адаптирован из этого сообщения на StackOverflow.

Шаг 3: Запустите тесты!

Вы можете просто использовать:

python run_unittests.py

для локального запуска тестов.

При желании вы также можете скопировать файл run_unittests.py в контейнер докеров. Вы можете SSH в свой контейнер докеров, используя

docker exec -it <container name> /bin/bash

а затем запустите тесты, используя after, чтобы перейти в каталог с этим файлом:

python run_unittests.py

Ууууу! Вы должны увидеть, что ваши тесты пройдены (надеюсь), или вы можете вернуться к своему коду и проверить свою работу.

Заключение к части 4

Надеюсь, вам понравилось реализовывать эту серию руководств. Вы можете найти весь написанный мной код на GitHub.

Это завершение проекта. Есть много онлайн-ресурсов, которые помогли мне разобраться в этом, и интернет всегда ваш друг, если вы застряли. Тем не менее, если у вас есть какие-либо вопросы или отзывы, оставьте их в комментариях, и я постараюсь помочь!

В этой серии:

Часть 1: Настройка нашего API
Часть 2: Интеграция Gunicorn, Nginx и Docker
Часть 3: Flask Blueprints - управление несколькими конечными точками
Часть 4: Тестирование ваш ML API

Привет! Спасибо за чтение. Немного обо мне. Я изучаю компьютерные науки в Университете Британской Колумбии, Канада. В основном я работаю над проектами машинного обучения, в основном НЛП. Еще я занимаюсь фотографией как хобби. Вы можете подписаться на меня в Instagram и LinkedIn или посетить мой сайт. Всегда открыт для возможностей 🚀