От Jane Saldanha и Nile Wilson

Специалисты по данным все чаще работают с междисциплинарными командами над внедрением моделей, улучшающих приложения. Наши сильные стороны, как правило, находятся на этапах исследования и разработки таких проектов, как исследование данных, предварительная обработка данных и обучение моделей. Однако по мере того, как мы двигаемся вниз по течению в области машинного обучения, для нас крайне важно внедрять передовые методы разработки программного обеспечения, такие как контроль версий и модульное тестирование, чтобы обеспечить функциональность и интеграцию кода.
Цель этой статьи — познакомить специалистов по обработке и анализу данных с методами тестирования, которым можно следовать для приложений машинного обучения при переходе от разработки к производству. В этой статье мы рассмотрим основные концепции тестирования, посмотрим, как мы можем реализовать эти этапы, используя любой из двух пакетов Python (pytest и unittest), и рассмотрим пример реализации тестирования регрессионной модели.
Основные понятия тестирования
Тестирование программного обеспечения — это способ проверки того, работает ли программное обеспечение так, как ожидает клиент или потребитель продукта. Существуют различные уровни тестирования программного обеспечения, каждый из которых мы рассмотрим в этом разделе и как показано на рисунке ниже. Чтобы помочь укрепить концепции, давайте дополним пояснения, связанные с некодовой системой, с которой многие из нас могут быть знакомы, — с велосипедом.

Модульное тестирование – это первый уровень тестирования, на котором тестируются независимые модули и функции, чтобы проверить, работают ли они должным образом. Важно тестировать детали независимо друг от друга, потому что это снижает стоимость и время диагностики ошибок на более поздних этапах тестирования. Для велосипеда такие вещи, как поддержание надлежащего давления в шине или звон колокольчика, можно рассматривать как модульные тесты.
Интеграционное тестирование — это этап, на котором отдельные функции объединяются и тестируются вместе, чтобы проверить, взаимодействуют ли они друг с другом и дают ли правильный результат. Для велосипеда это может быть вращение педалей, чтобы колеса вращались.
Тестирование системы включает в себя тестирование всей системы или продукта. Для велосипеда это может быть поворот руля при движении педалей, чтобы обеспечить поворот велосипеда.
Приемочное тестирование проводится для оценки того, соответствует ли работа системы бизнес-требованиям. В примере с велосипедом могут быть различные типы потребностей пользователей. И дорожный велосипед, и горный велосипед могут пройти системное тестирование, но имеют разные требования пользователя.
Покрытие тестами — это показатель, используемый для определения процента кода приложения, покрываемого тестовыми наборами, в основном с использованием модульных тестов. Охват определяет, является ли тестирование продукта эффективным и, в свою очередь, имеет ли полученный продукт хорошее качество. Покрытие тестами помогает выявлять ошибки в приложениях на ранней стадии, выделяя области кодовой базы, которые не охватываются тестовыми наборами, и помогает оптимизировать тестирование, удаляя избыточные или неприменимые тестовые наборы. С точки зрения примера с велосипедом, это может быть контрольный список проверки, показывающий каждый компонент, который еще не прошел какую-либо проверку качества или безопасности.
В этой статье мы сосредоточимся в первую очередь на фундаментальных уровнях тестирования, а именно модульном тестировании и интеграционном тестировании, чтобы специалисты по данным чувствовали себя вправе разрабатывать тесты для кода, который они могут написать как часть более крупного инженерного решения.
Средства тестирования, предоставляемые Python
Как упоминалось выше, Python предоставляет две среды для тестирования: unittest и pytest. Давайте изучим эти две библиотеки.
единичный тест
Python использует библиотеку unittest в качестве библиотеки тестирования по умолчанию. Тестовые наборы, созданные с помощью этой библиотеки, относятся к классу, унаследованному от unittest.TestCase. Он поддерживает объединение тестов в коллекцию для автоматизации тестирования.
Команды, используемые для выполнения тестовых случаев, написанных с использованием фреймворка unittest, следующие:
- Для параметров командной строки:
python -m unittest -h - Запуск тестов из различных модулей с помощью CLI:
python -m unittest test_module_1 test_module_2 - Выполнение тестов из одного тестового класса:
python -m unittest test_module.TestClass1 - Выполнение отдельных тестовых функций:
python -m unittest test_module.TestClass.test_method
Pytest
Pytest, вдохновленный библиотекой Junit для Java, представляет собой более легкую среду, которая требует меньше кода благодаря своим богатым функциям. Его можно использовать для широкого спектра тестов, таких как тестирование API, интеграция пользователей, тестирование баз данных и многое другое. Pytest предоставляет отчеты о покрытии на терминалах, в которых выделяются утверждения во включенных файлах, не охваченные тестовыми примерами, охваченные утверждения и процент покрытия. Он также предоставляет статус тестовых случаев для всего проекта. Другими форматами, в которых может быть сгенерирован отчет, являются HTML, XML и аннотированный исходный код.
Команды, используемые для выполнения тестов с использованием среды pytest, следующие:
- Выполнение тестового файла:
pytest filename.py - Выполнение конкретного теста файла:
pytest имя_файла.py::testcasename
Пример
Рассмотрим следующую функцию, которую необходимо протестировать.
Соответствующий тестовый пример для функции в unittest выглядит следующим образом:
Как видно из приведенного выше фрагмента, для написания тестовых случаев с использованием библиотеки unittest нам требуется набор классов, наследуемый от пакета unittest.TestCase. Unittest использует встроенную функцию assert, которая проверяет определенное условие. Если утверждение терпит неудачу, это приводит к ошибке утверждения.
Теперь давайте посмотрим на тестовый пример для той же функции square() с использованием pytest:
Приведенный выше фрагмент более краток и удобочитаем по сравнению с тестовым примером, написанным с использованием фреймворка unittest. Утверждение выполняется с помощью ключевого слова assert, логических операторов (==, !=, ‹=) и операторов принадлежности (in, not in) в зависимости от проверяемого условия.
Одним из сходств между pytest и unittest является номенклатура тестовых случаев: они оба должны начинаться с test_ или заканчиваться на _test, чтобы быть распознанными как тестовые примеры во время выполнения.
Насмешки: важная концепция в тестировании
Насмешка — это процесс замены объекта на место зависимости, которая имеет определенные ожидания. Эти ожидания могут включать возврат некоторых математических вычислений, проверку подметода или даже подключение к внешней среде (например, к командам Azure). При написании модульных тестов важно убедиться, что они независимы от других функций. Следовательно, крайне важно смоделировать все зависимости, чтобы убедиться, что тестируемая функция работает правильно.
Рассмотрим следующую функцию, которая зависит от функции квадрата:
Теперь давайте рассмотрим, как писать примеры модульных тестов, используя обе платформы. Фреймворк unittest предоставляет фиктивную библиотеку, которая импортируется с помощью кода из макета импорта unittest.
Давайте посмотрим, как будет выглядеть тестовый пример, написанный с использованием фреймворка pytest.
Библиотека pytest-mock предоставляет имитатор, который используется для исправления зависимых функций. Исправление означает замену зависимой функции другой функцией, которая имитирует функциональность или возвращает выбранное нами значение. Мы рассмотрим, какие приспособления есть в следующем разделе.
Светильники
Фикстуры — это функции, которые запускаются перед каждой тестовой функцией, к которой они применяются. Они передаются в качестве параметра в тестовый пример и используются для передачи некоторых данных в соответствующий тестовый пример. Передаваемые данные могут быть, среди прочего, URL-адресом, фреймом данных или подключением к базе данных. Функция может быть объявлена как фикстура с помощью @pytest.fixture. Рассмотрим функцию:
Чтобы протестировать приведенную выше функцию, нам пришлось бы смоделировать функцию new_add, которая отвечает за добавление нового столбца. Мы можем использовать фикстуры, чтобы добавить новый столбец, имитируя функцию. Приспособление и тестовый пример приведены ниже.
Полный пример тестового кода по науке о данных
Чтобы продемонстрировать тестирование кода науки о данных, давайте рассмотрим простую задачу регрессии, в которой мы прогнозируем индекс стоимости жизни, используя различные параметры, такие как рейтинг, город, индекс арендной платы, индекс стоимости жизни плюс арендная плата, индекс бакалейных товаров, индекс цен в ресторанах и местный индекс. Индекс покупательной способности (Jazz4299/PYTEST (github.com)).
Задача регрессии выполняется с использованием файла process.py в папке src. Сначала он вызывает модуль приема данных, который ожидает путь и возвращает ошибку, если указанный путь неверен или не существует. Затем вызывается модуль plot, который создает графики для исследовательского анализа и предварительно обрабатывает данные (включая удаление нулевых значений, разбиение столбца city на столбцы city и country и кодирование категориальных значений). Последний модуль — это построение обучающей модели, которое отвечает за разделение набора данных, обучение модели, прогнозирование выходных данных и создание визуализации производительности модели.
Давайте рассмотрим тестирование этой задачи науки о данных.
Модульное тестирование с использованием pytest
В файле dataloading.py есть одна функция, содержащая два сценария: в одном указан правильный путь, а в другом вводится неверный путь.
Давайте рассмотрим сценарий, в котором указан правильный путь:
Каждый тестовый набор начинается с test_ или заканчивается _test, чтобы pytest распознал его как тестовый набор. Тестовый пример вызывает функцию load_data(), которая является тестируемой функцией, и проверяет, является ли возвращаемый объект фреймом данных. Если указан неверный путь, функция возвращает исключение, и его можно обработать с помощью pytest.raises(), как показано во фрагменте ниже.
Мы видим декоратор поверх тестового примера, pytest.mark.parametrize, который помогает объявлять несколько наборов аргументов и фикстур для тестовой функции.
В файле plots.py есть две функции, хранящие гистограмму и тепловую карту. Эти функции вызываются в функции _init_, а это означает, что для проверки одной из функций необходимо смоделировать другую.
Давайте рассмотрим тестовый пример для hist_observation(). Как показано ниже, тепловая карта функции имитируется с помощью прибора-мокера. Затем тестовый пример проверяет, создан ли файл histogram.png в том месте, где он был предназначен.
Класс ModelBuilding вызывает функции разделения, обучения и прогнозирования в функции _init_ и создает экземпляр объекта Plot. Чтобы протестировать вышеуказанные функции, объект Plot должен быть смоделирован. Этого можно добиться с помощью функции mocker.patch.object(). Тестовый пример для test_split() приведен ниже:
Интеграционные тесты
Когда дело доходит до интеграционных тестов для кода науки о данных, то, как запускается фактический тест, зависит от структуры вашей кодовой базы и решения, находящегося в производстве. Напомним, модульные тесты проверяют, что отдельные функции работают должным образом, а интеграционные тесты гарантируют, что функции взаимодействуют друг с другом должным образом, пока выполняется все решение.
Интеграционные тесты, как и модульные тесты, следует запускать перед отправкой кода в производственную среду. С CI/CD это может выглядеть как автоматический запуск интеграционных тестов каждый раз, когда создается или обновляется запрос на вытягивание, и блокировка слияния функциональной ветки со средой (dev, QA или prod) до тех пор, пока все тесты не будут пройдены. Итак, как выглядит интеграционный тест? Интеграционные тесты, по сути, запускают все ваше решение на небольшом подмножестве ваших данных (например, игрушечном наборе данных), чтобы вы могли наблюдать за поведением и убедиться, что все работает должным образом. Лучшей практикой является использование подмножества данных, чтобы гарантировать, что это работает быстрее, чем при использовании полного набора данных. В сценарии MLOps интеграционные тесты могут выглядеть как файл yml, в котором указан основной файл Python оркестратора для запуска и его входные данные. В приведенном ниже фрагменте кода мы видим, что наш основной файл Python (CostOfLiving/src/process.py в примере репозитория) запускает все решение и принимает несколько различных входных аргументов, в том числе какой файл csv считывать в качестве входных данных. источник.
Вместо того, чтобы передавать полный CSV-файл набора данных, мы бы указали меньший CSV-файл игрушечного набора данных в файле yml. Чтобы запустить интеграционный тест во время CI/CD, файл yml будет использоваться для выполнения кода с игрушечным набором данных, создавая интеграционный тест. Хотя в этой статье мы не рассматриваем построение конвейера и настройку репозитория MLOps, мы отмечаем, что это можно сделать на различных платформах, включая, помимо прочего, GitHub, GitLab и Azure DevOps. Некоторые примеры, применимые к решениям, использующим службы Azure, можно найти в Документации по машинному обучению Azure Microsoft.
Заключение
В этой статье мы рассмотрели основы тестирования кода обработки данных и предоставили реальный пример, демонстрирующий принципы на практике с использованием фреймворков тестирования Python unittest и pytest. Мы надеемся, что с этими объяснениями и примерами, которые охватывают различные сценарии, которые могут возникнуть во время разработки кода для обработки данных, вы почувствуете, что у вас есть возможность разрабатывать собственные тесты для приложений обработки данных.