Добрый день уважаемые жители !
Работая на текущем проекте, я столкнулся с проблемой однотипных данных и необходимостью протестировать разные http вызовы над одними и теми же данными.
Проект разрабатывается на django/django-rest-framework/python3.5.
Поначалу я начал использовать этот движок для упрощения тестирования django-rest-framework (django-rest-assured — https://github.com/ydaniv/django-rest-assured).
Но, имея необходимость протестировать по сути одни и те же данные на разных урлах, я осознал, что использование этого движка не помогло так уж сильно облегчить задачу.
Конечно, в какой то мере проект стал более податливым для тестирования. Но, возникало много вопросов с так называемыми django-rest-framework detail_route и list_route. Тем, кто не в курсе напомню, эти декораторы позволяют определить специфические действия выполняющиеся над каким то конкретным типом данных (дальше, ресурсом).
И потом я понял, что тесты в проекте нужно группировать относительно ресурсов (типов) данных, над которыми они тестируются.
А есть ли реальная необходимость в этом ?
К примеру, возьмем объект машина, стоит цель продать машину, какие действия могут быть выполнены над ней и какие типы запросов могут быть в отношении этого объекта (машина):
- в исправном ли она состоянии ?
- если в неисправном, то как дорого и долго будет ее восстановить ?
- если в исправном, то проверка ее статуса с определенной периодичностью
- сделать ставку в отношении машины, товара (если мы говорим об аукционах)
Это все будет одна машина, одна запись из базы данных.
Но разные http запросы. Я согласен, что может быть не совсем удачный пример с машиной, ибо http запросами не оценить стоимость восстановления машины… Да и понятно же что много из этих данных могут быть возвращены при GET запросе касательно этой машины (и дополнительные http запросы могут не потребоваться), но предположим это не так. И тут возникает проблема: у нас одна машина, но куча тестов для каждой из операций над этой машиной. Почему бы не сгруппировать все эти тесты в один набор тестов но над одним ресурсом.
Я приведу пример xml, который можно было бы написать для набора таких тестов:
<resource id="normal_car">
<rest url="/api/core/cars/{id}/is_fine/" method="get" />
<rest url="/api/core/cars/{id}/repair_cost_and_time/" method="get" />
<rest url="/api/core/cars/{id}/is_available/" method="get" />
<rest url="/api/core/cars/{id}/bet/" method="post" />
</resource>
Сразу замечу, почему я здесь привожу ссылку с {id}, я использую в easytest движке open api схему для генерации фикстур для запроса и тд и тп.
И в openapi схеме ссылка для detail запросов связанных с конкретных ресурсом генерируются как:
"/api/core/cars/{id}/bet/": {
"post”: {
"consumes": [
"application/json"
],
"description": "Description",
"operationId": "operationId",
"parameters": [
{
"description": "",
"in": "path",
"name": "id",
"required": true,
"type": "string"
},
{
"in": "body",
"name": "data",
"schema": {
"properties": {
"price": {
"description": "",
"type": "integer"
},
},
"required": [
"price"
],
"type": "object"
}
}
],
}
}
И что дальше ?
Затем, имея open api схему, мы можем:
- сгенерировать фикстуру для данного http вызова, например, в случае с bet/ это может быть: {‘price’: 1000}
- сделать нужный http вызов и проверить результат, который вернул сервер.
Именно для этого я сделал easytest движок.
Во первых, никакого кода для тестов не генерируется. Зачем нам код для тестов если схема для тестов описана уже в open api схеме? Единственное что нам нужно – это возможность определять кастомные setup, teardown, matcherы для http вызовов, и это все что нам нужно иметь возможность делать для осуществления тестирования. Кстати, нужно еще не забывать что разные http вызовы могут требовать разного режима базы данных, некоторые — READ_UNCOMMITED, некоторые — READ COMMITED. Это тоже учтено в движке.
Теперь по порядку о том как все работает:
- мы определяем схему тестирования для нашего приложения
Вот пример из тестов проекта:
bitbucket xml - Вот dtd для валидации схемы тестирования:
ссылка на dtd файл
Как вы видите, запросы могут быть anonymous, transactional.
На данный момент поддерживается только один framework в движке для тестирования: drf — Это django rest framework. Но, добавление нового frameworkа очень просто, нужно переопределить два класса:
bitbucket drf specific request, test visitor classes - Очевидно, что разные типы данных могут иметь разные требования по валидации, например пароли, как правило эти правила валидации уникальны для проекта, для этого мы имеем возможность регистрировать кастомные data генераторы:
ссылка на код
Причем, эти кастомные генераторы имеют доступ к текущему requestу, так что они могут вернуть корректные данные для разных эндпоинтов. Таким образом, мы можем реализовать практически любые требования по валидным, инвалидным данным в движке для тестирования. - Теперь об одной из ключевых вещей для проекта:
это реестр ресурсов:
есть один ресурс, который необходим всегда, в случае если у вас есть хоть один не анонимный тест (запрос):
current_user → пример с того как это реализовано в тесте для django
ссылка на код - Кастомные teardown, setup, custom matcher возможно определить следующим образом:
ссылка на код
Однако, вы должны определить один тест, в котором вы сделаете инициализацию easytest и напишете что то подобное:
кликните, чтобы перейти на пример теста для django + drf
Как понять что ошибка произошла ?
На сегодняшний момент, в случае падения теста происходит вывод того что упало, на каком эндпоинте, и traceback exceptionа в консоль (терминал) в красном цвете. И если хоть один тест упал, то падает весь тест test_resources с следующим сообщением:
Automatic test of endpoints has been failed
Пока еще не опубликовал пакет на pypi. Еще думаю над названием, но уже использую в своем проекте, в котором порядка 100 эндпоинтов. Сильно облегчает жизнь
И да, решение не идеальное
Из недостатков:
- добавить комментарии, type hints для упрощенной навигации по коду, помогает сильно в редакторах где есть хороший python type hinting
- реализовать настоящий инжектор специфичных для движка классов. Пока инжектинг работает просто как локатор. Но есть идея заюзать pypi пакет injector. Правда я хочу чтобы пакетом пользовались и под python2 (хотя еще не проверял под python2). С маленькими фиксами будет работать
- набросайте еще недостатков по коду и подходу к решению проблемы
Автор: незнакомец