|
|
||
Тестирование API - это тип интеграционных тестов, представляющих полноценное тестирование для SaaS продуктов. В контексте интеграционных тестов сложной задачей является зависимость компонентов. Хороший интеграционный тест хорошо проверяет взаимодействие между несколькими компонентами или API. Кроме того, создание герментичных окружений полезно для повышения эффективности тестирования и уверенности в проведении интеграционных тестов.
Данный документ поможет построить архитектуру pytest. Основные модели будут разделены между тремя контроллерами Module, Logic, Validation. В принципе, все контроллеры должны работать над главной целью.
Человекочитаемый интерфейс для разработки тестовых шагов в этом модуле.
В этом объекте (классе) мы фокусируемся на пользовательском сценарии, каждый тест-кейс содержит четкие шаги с действиями или проверками. В качестве тестового фреймворка можем использовать фикстуру pytest для инициализации и финализации модуля. Кроме того, с помощью этого механизма можно задать жизненный цикл и scope с большой гибкостью.
Следующий пример показывает как выглядит Test Case Module.
from controller.example_feature.users import *
import pytest
@pytest.fixture(scope="function", autouse=True)
def prepare_params(setup):
routed_path = '/users/weigo'
api_url = f'{setup.url_prefix}{routed_path}'
setup.logic_controller = Users(api_url, setup.dataset)
yield setup
class TestGetUsers:
def test_rat_get_users(self, setup):
# Provide Steps
result = setup.logic_controller.get_users()
# Provide Verfication
UsersAssertion.verify_general_response_code_200(result)
Этот шаблон проектирования аналогичен page object паттерну. Если веб-страница является единицей класса, API - похожая концепция что и page object. Каждый API должен содержать определенные методы и функции. Кроме того, мы должны учесть зависимость между двумя API. Лучшая практика - попытаться изолировать каждый API. Преимущество заключается в следующем:
Стандартный restful API может иметь несколько http-методов вроде GET, POST, PUT, PATCH, DELETE. Для построения логического контроллера используется название роута URL вроде https://api.github.com/users/weigo
Согласно пути выше мы будем использовать users в качестве имени логического контроллера, затем создадим класс Users(Base) и класс UsersAssertion(BaseAssertion). Все функции в классе users связаны с конкретной функцией API, структурой данных и наборе действий для работы с этой функцией.
from controller.api_util.base_logic_controller import Base, BaseAssertion
class Users(Base):
def __init__(self, url, data_set):
Base.__init__(self)
self.data_set = data_set
self.url = url
def get_users(self):
res = self.send_request(
Base.RequestMethod.GET,
custom_url=self.url
)
return res
class UsersAssertion(BaseAssertion):
@classmethod
def verify_specific_results(cls, res: Base.ResponseObject):
# Here to verify specific results from Response Object
pass
класс Base создается для общих или базовых функций общего доступа API, например: реализация отправки, обработка вложений или обработчик ответа.
from .common_imports import *
import uuid
import requests
from requests.models import Response
class Base(object):
class ResponseObject(object):
def __init__(self, response: Response):
self.status_code = response.status_code
self.content = response.content
self.text = response.text
try:
self.json = response.json()
except Exception as e:
self.json = None
logger.warning(e)
self.header = response.headers
self.url = response.url
class RequestMethod(str, Enum):
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
PATCH = "PATCH"
def __init__(self) -> None:
pass
@staticmethod
def gen_unique_str():
id = uuid.uuid4()
return id
def send_request(self,
method: RequestMethod = RequestMethod.GET,
payload=None,
chunk_size: int = 0,
cookies=None,
custom_url: str = None,
headers=None,
files: list = None
) -> ResponseObject:
_payload = None
if files:
_payload = {}
if custom_url is None:
logging.warning("should provide url when sending request")
if payload is None:
logging.warning("should provide payload when sending request")
else:
_payload = payload
if headers is None:
_headers = {"Content-Type": "application/json"}
else:
_headers = headers
# new request session
res = None
session_res = requests.session()
if method is self.RequestMethod.GET:
res = session_res.get(custom_url, headers=_headers, cookies=cookies, stream=True)
elif method is self.RequestMethod.POST:
res = session_res.post(custom_url, headers=_headers, cookies=cookies, stream=True, json=_payload, files=files)
elif method is self.RequestMethod.PUT:
res = session_res.put(custom_url, headers=_headers, cookies=cookies, stream=True, json=_payload, files=files)
elif method is self.RequestMethod.DELETE:
res = session_res.delete(custom_url, headers=_headers, cookies=cookies, stream=True)
elif method is self.RequestMethod.PATCH:
res = session_res.patch(custom_url, headers=_headers, cookies=cookies, stream=True, json=_payload, files=files)
else:
res = session_res.put(custom_url, headers=_headers, cookies=cookies, stream=True, data=_payload, files=files)
res_obj = self.ResponseObject(res)
# Formalize output logs for all API tests
logger.info("\n=============URL=================\n")
logger.info(res_obj.url)
logger.info("\n============Payload==============\n")
logger.info(payload)
logger.info("\n============Response=============\n")
if len(res.content) < 10000:
logger.info(res_obj.text)
else:
logger.info("Too large response body, skipped to print.")
logger.info("\n=================================\n")
return res_obj
В идеале каждый API интерфейс должен иметь собственную логику проверки, поэтому мы ожидаем, что каждый логический контроллер будет иметь класс проверки. Кроме того, мы должны четко разделить уровень уровень функции и тестовой проверки, чтобы каждый контроллер работал над своей целью. С другой стороны, иногда API имеет сложные комплексные шаги валидации и если мы реализуем их в одном и том же месте, поддерживать наши тест-кейсы может быть сложно. С помощью этого шаблона проектирования мы можем дать возможность писать разработчику правильный код в нужном месте.
Структура класса валидации такая же как у логического контроллера. Все классы проверки наследуются от BaseAssetion, куда можно встраивать общие функции проверки или базовые функции. Например: проверка 200, 500 кодов, плохой запрос 400 или контент не в json формате.
Какова лучшая практика для реализации проверок?
Предположим, что каждый ответ API будет иметь ожидаемые результаты и в идеале каждый API должен работать автономно с четкими входными данными, эта логика будет помещена в контроллер валидации и использовать функцию assert для проверки результатов.
Убедитесь, что ваш API может быть протестирован без каких-либо зависимостей, чтобы провести автономное тестирование API есть два подхода:
В этой статье мы используем yml для хранения предварительно подготовленных тестовых данных в {root}/testdata/*.yml
когда вы запускаете pytest для определенного набора тестов, укажите путь в качестве параметра в командной строке
pytest ./testsuite/example_feature/test_get_users.py --env=STAGING --dataset={feature_folder}/test_data
Вы можете скачать Демо-проект для реализации вашей собственной интеграционной платформы API.
Особенности:
|
Новые книги авторов СИ, вышедшие из печати:
О.Болдырева "Крадуш. Чужие души"
М.Николаев "Вторжение на Землю"