Добрый день. Я занимаюсь автоматизацией тестирования. Как и у всех автоматизаторов, у меня есть набор библиотек и инструментов, которые я обычно выбираю для написания тестов. Но периодически возникают ситуации, когда ни одна из знакомых библиотек может решить задачу с риском сделать автотесты нестабильными или хрупкими. В этой статье я хотел бы рассказать, как вроде бы стандартная задача использования mock'ов привела меня к написанию своего модуля. Также хотел бы поделиться своим решением и услышать обратную связь. Одной из необходимых отраслей в финансовом секторе является аудит. Данные необходимо регулярно сверять (reconciliation). В связи с этим и появилось то приложение, которое я тестировал. Чтобы не говорить о чём-то абстрактном, представим, что наша команда разрабатывает приложение для обработки заявок из мессенджеров. Для каждой заявки должно быть создано соответствующее событие в elasticsearch'е. Сверяющее же приложение будет нашим мониторингом, что заявки не пропускаются. Итак, представим, что у нас есть система, имеющая следующие компоненты: Автоматизация тестирования в данном случае выглядит достаточно прямолинейно: Проблема заключается в том, что мы тестируем мониторинг, но чтобы его настроить, нужны данные от сервера конфигурации. Во-первых, устанавливать и настраивать сервер для каждого прогона — трудоёмкая операция (он имеет свою базу, например). Во-вторых, хочется изолировать приложения, чтобы упростить локализацию проблем при нахождении дефекта. В итоге, было решено использовать mock. Тут может возникнуть вопрос: "Если мы всё равно будем mock'ать сервер, может и на установку и наполнение elasticsearch'а время не тратить, а заменить mock'ом?". Но всё-таки всегда нужно помнить, что использование mock'ов предоставляет гибкость, но добавляет обязательства следить за актуальностью поведения mock'а. Поэтому от замены elasticsearch'а я отказался: его и устанавливать, и наполнять достаточно просто. Сервер отдаёт конфигурацию на GET-запросы по нескольким путям в /configuration. Нас интересуют два пути. Первый — это /configuration/data_cluster с конфигурацией кластера Второй — это /configuration/reconciliation с конфигурацией сверяющего приложения Сложность заключается в том, что нужно иметь возвожность изменять ответ сервера во время теста или между тестами, чтобы тестировать то, как приложение реагирует на изменение конфигурации, неверные пароли и т.п. Итак, статические mock'и и средства для mock'ов в юниттестах (mock, monkeypatch из pytest'а и т.п.) нам не подойдут. Я нашёл отличную библиотеку pretenders, которая, как я думал, мне подходит. Pretenders предоставляет возможность создавать HTTP сервер с правилами, которые определяют, как сервер будет отвечать на запросы. Правила хранятся в preset'ах, что позволяет изолировать mock'и для разных наборов тестов. Preset'ы можно очищать и заполнять заново, что позволяет обновлять ответы по необходимости. Сам сервер достаточно поднять один раз во время подготовки окружения: А в тесты нам нужно добавить использование клиента. В самом простом варианте, когда ответы целиком захардкожены в тестах, это может выглядеть так: Но это ещё не всё. При своей гибкости, pretenders имеет два ограничения, о которых надо помнить и необходимо решить в нашем случае: Первое ограничение очень неприятное, но может быть решено написанием модуля, который инкапсулирует работу с конфигурацией. Например, так или (чтобы делать запросы на обновление реже) Такая инкапсуляция позволяет нам практически безболезненно обновлять все пути. Также можно создать индивидуальный preset для каждого отдельного endpoint'а и общий (основной) preset, перенаправляющий (через 307 или 308) на индивидуальные. Тогда можно очищать только один preset для обновления правила. Для того, чтобы избавиться от префиксов, можно воспользоваться библиотекой mitmproxy. Это мощный инструмент, который позволяет, помимо прочего, перенаправлять запросы. Мы уберём префиксы следующим образом: Параметры этой команды делают следующее: В нашем случае, мы добавляем mockhttp/server ко всем HTTP-запросам и перенаправляем их на http://127.0.0.1:8000, т.е. на наш сервер pretenders. В итоге мы добились того, что теперь конфигурацию можно получить GET-запросом к http://127.0.0.1/configuration/data_cluster. В целом меня устраивала конструкция с pretenders и mitmproxy. При кажущейся сложности — всё-таки 2 сервера вместо одного настоящего — подготовка заключается в установке 2 пакетов и выполнении 2 команд в командной строке для их запуска. В управлении mock'ом не всё так однозначно, но вся сложность заключена только в одном месте (управлении preset'ами) и решается достаточно просто и надёжно. Однако в задаче появились новые обстоятельства, которые заставили меня задуматься о новом решении. До этого момента я почти ничего не говорил, откуда берутся эталонные данные. Внимательный читатель мог заметить, что в примере выше адресом источника данных используется путь на файловой системе. И это действительно примерно так и работает, но только для одного из вендоров. Другой же вендор предоставляет API для получения заявок, и именно с ним возникла проблема. API вендора затруднительно поднимать во время тестов, поэтому я планировал его заменить mock'ом по той же схеме, что и раньше. Но для получения заявок используется запрос вида Тут настораживают 2 момента. Во-первых, несколько параметров. Дело в том, что параметры могут быть указаны в любом порядке, что сильно усложняет регулярное выражение для правила pretenders. Ещё необходимо помнить, что параметры необязательные, но это не такая проблема, как произвольный порядок. Во-вторых, последние параметры (start и end) задают временной промежуток для фильтрации заявки. И проблема в данном случае в том, что мы не может заранее предугадать, какой промежуток (не величину, а время старта) будет использован приложением, чтобы сформировать ответ mock'а. Проще говоря, нам необходимо знать и использовать значения параметров, чтобы сформировать "разумный" ответ. "Разумность" в данном случае важна, например, чтобы мы могли протестировать, что приложение проходит по всем страницам пагинации: если на все запросы мы будем отвечать одинаково, то мы не сможем найти дефектов, связанных с тем, что запрашивается только одна страница из пяти. Я попробовал поискать альтернативные варианты решения, но в итоге решил попробовать написать своё. Так появился loose server. Это Flask-приложение, у которого пути и ответы могут быть настроены после его старта. "Из коробки" он умеет работать с правилами для типа запроса (GET, POST и т.п.) и для пути. Это позволяет заменить pretenders и mitmproxy в изначальной задаче. Также я покажу, как его можно использовать, чтобы создать mock для API вендора. Приложению необходимы 2 основных пути: В общем случае, лучше не настраивать base endpoint и configuration endpoint так, чтобы один был родителем другого. Иначе есть риск, что пути для конфигурации и для тестирования будут конфликтовать. Приоритет будет у configuration endpoint, так как Flask-правила для конфигурации добавляются раньше, чем для динамических путей. В нашем случае можно было бы использовать --base-endpoint /configuration/, если бы мы не собирались включать API вендора в этот mock. Простейший вариант тестов не сильно меняется Фикстура стала сложнее, но правила теперь можно удалять по одному, что упрощает работу с ними. Использование mitmproxy уже не нужно. Вернёмся теперь к API вендора. Мы создадим новый тип правил для loose server'а, который в зависимости от значения параметра будет отдавать различные ответы. Далее мы воспользуемся этим правилом для параметра page. Новые правила и ответы нужно создавать как для сервера, так и для клиента. Начнём с сервера: Каждое правило должно определить метод is_match_found, которое определяет, должно ли оно сработать для данного запроса или нет. Входным параметром для него является объект запроса. После того, как новое правило создано, надо "научить" сервер принимать его от клиента. Для этого используется RuleFactory: Здесь мы создаём фабрику для правил по умолчанию, чтобы она содержала те правила, которые мы использовали раньше, и регистрируем новый тип. Клиенту в данном случае информация о правиле не нужна, поэтому serializer ничего фактически не делает. Далее эта фабрика передаётся в приложение. И его уже можно запускать, как обычное Flask-приложение. С клиентом ситуация аналогичная: создаём правило и фабрику. Но для клиента, во-первых, не нужно определять метод is_match_found, а, во-вторых, serializer в данном случае необходим, чтобы отправить правило серверу. Осталось воспользоваться _create_client для создания клиента, и правила можно использовать в тестах. В примере ниже я добавил использование ещё одного правила по умолчанию: CompositeRule. Оно позволяет объединять несколько правил в одно так, что они срабатывают, только если каждое из них возвращает True для вызова is_match_found. Связка библиотек pretenders и mitmproxy предоставляет мощный и достаточно гибкий инструмент для создания mock'ов. Его плюсы: К минусам можно отнести: Ссылки на документацию:
Pretenders
Mitmproxy
Loose server