
Привет, я Алексей, QA Automation Engineer в команде «Интеграции» в Петрович-ТЕХ. Занимаюсь разработкой фреймворка автоматизированного тестирования сервисов интеграции, для REST и SOAP.
Наблюдение: когда приходишь на собеседование на должность Junior QA Automation, то обязательно просят разработать автотесты для API. Звучит логично, но не так уж и просто: когда только начинаешь свой путь в автотестировании, тебе не всегда очевидно, как должен выглядеть рабочий тестовый фреймворк, из чего он должен состоять, как правильно написать тесты, а к ним тестовые данные. «Сырые» тесты, которые описывают в книгах и разных источниках – не всегда выручают.
В этой статье расскажу о разработке типового фреймворка для тестирования API – на Python, с нуля, шаг за шагом. В итоге получим полностью готовый тестовый фреймворк – надеюсь, с его помощью вы сможете сделать тестовое задание для собеседования или просто улучшить ваш уже действующий тестовый фреймворк.
Надеюсь, статья будет интересна начинающим авто-тестировщикам и тем, кто уже разрабатывает автотесты для API.
Постановка задачи
Для наших целей воспользуемся открытым API – ReqRes.
В статье я не буду описывать все методы выбранного API; ограничусь методами CRUD, как основными. Для примера этого будет вполне достаточно; для других методов делается по образу и подобию.
Методы, для которых будем писать тесты: Get, Post, Put, Delete.
Репозиторий проекта: https://github.com/ScaLseR/petrovich_test.
С вводными условиями определились, давайте приступать.
Реализуем основной класс API
В корне проекта создадим директорию «api», а в ней – файл «api.py». Опишем там основной класс для работы с API – там будет реализована логика отправки запросов и будет обрабатываться полученный ответ. Класс так и назовем – «Api».
class Api: """Основной класс для работы с API""" _HEADERS = {'Content-Type': 'application/json; charset=utf-8'} _TIMEOUT = 10 base_url = {} def __init__(self): self.response = None
В корне проекта добавлен файл requirements.txt, в котором будем хранить список необходимых библиотек.
Библиотеки, с которыми будем работать:
import allure import requests from jsonschema import validate
Requests – поможет нам с отправкой запросов и получением ответов.
Allure – добавит в наш проект возможность формирования отчетов в Allure. Это позволит получать удобный, хорошо читаемый отчет о тестировании.
Jsonschema – отсюда импортируем функцию validate, для реализации проверки на соответствие схеме.
В нашем классе Api реализуем функциональность отправки запросов и получения ответов. Для POST-запроса код будет выглядеть следующим образом:
def post(self, url: str, endpoint: str, params: dict = None, json_body: dict = None): self.response = requests.post(url=f"{url}{endpoint}", headers=self._HEADERS, params=params, json=json_body, timeout=self._TIMEOUT) return self
Добавим «@allure.step” – будем передавать шаги в наш Allure-отчёт.
@allure.step("Отправить POST-запрос") def post(self, url: str, endpoint: str, params: dict = None, json_body: dict = None): with allure.step(f"POST-запрос на url: {url}{endpoint}" f"\n тело запроса: \n {json_body}"): self.response = requests.post(url=f"{url}{endpoint}", headers=self._HEADERS, params=params, json=json_body, timeout=self._TIMEOUT) return self
Дополнительно будем логировать requests и responses. Для этого добавим в проект директорию «helper» – там будут содержаться все наши дополнительные модули. Напишем первый модуль для логирования – logger.py.
"""Модуль логирования""" import logging logger = logging.getLogger() logger.setLevel(logging.INFO) def log(response, request_body=None): logger.info(f"REQUEST METHOD: {response.request.method}") logger.info(f"REQUEST URL: {response.url}") logger.info(f"REQUEST HEADERS: {response.request.headers}") logger.info(f"REQUEST BODY: {request_body}\n") logger.info(f"STATUS CODE: {response.status_code}") logger.info(f"RESPONSE TIME: {response.elapsed.total_seconds() * 1000:.0f} ms\n") logger.info(f"RESPONSE HEADERS: {response.headers}") logger.info(f"RESPONSE BODY: {response.text}\n.\n.")
Модуль готов, добавим его использование в наш класс Api:
from helper.logger import log
Добавим логирование для наших методов; код метода POST будет иметь следующий вид:
@allure.step("Отправить POST-запрос") def post(self, url: str, endpoint: str, params: dict = None, json_body: dict = None): with allure.step(f"POST-запрос на url: {url}{endpoint}" f"\n тело запроса: \n {json_body}"): self.response = requests.post(url=f"{url}{endpoint}", headers=self._HEADERS, params=params, json=json_body, timeout=self._TIMEOUT) log(response=self.response, request_body=json_body) return self
Отлично, теперь мы можем отправлять реквесты с нужными данными и получать респонсы.
Для проверки полученных респонсов в тестах нам понадобятся ассерты, их также добавим в основной класс Api. Добавим несколько самых основных.
-
На соответствие статус-кода
@allure.step("Статус-код ответа равен {expected_code}") def status_code_should_be(self, expected_code: int): """Проверяем статус-код ответа actual_code на соответствие expected_code""" actual_code = self.response.status_code assert expected_code == actual_code, f"\nОжидаемый результат: {expected_code} " \ f"\nФактический результат: {actual_code}" return self
-
На соответствие ответа json-схеме
@allure.step("ОР: Cхема ответа json валидна") def json_schema_should_be_valid(self, path_json_schema: str, name_json_schema: str = 'schema'): """Проверяем полученный ответ на соответствие json-схеме""" json_schema = load_json_schema(path_json_schema, name_json_schema) validate(self.response.json(), json_schema) return self
Для реализации проверки ответа на соответствие схеме необходимо добавить в наш «helper» ещё один модуль – load.py. В нём добавим функцию load_json_schema – для подгрузки нужной json-схемы из файла. Модуль будет иметь вид:
"""Модуль для работы с файлами""" from importlib import import_module def load_json_schema(path: str, json_schema: str = 'schema'): """Подгрузка json-схемы из файла""" module = import_module(f"schema.{path}") return getattr(module, json_schema)
Не забываем добавить новый модуль из «helper» в класс Api.
from helper.load import load_json_schema
-
На соответствие объектов
Будем десериализировать полученный ответ в объект и сравнивать с эталонным.
@allure.step("ОР: Объекты равны") def objects_should_be(self, expected_object, actual_object): """Сравниваем два объекта""" assert expected_object == actual_object, f"\nОжидаемый результат: {expected_object} " \ f"\nФактический результат: {actual_object}" return self
-
На соответствие значения для определенного параметра
@allure.step("ОР: В поле ответа содержится искомое значение") def have_value_in_response_parameter(self, keys: list, value: str): """Сравниваем значение необходимого параметра""" payload = self.get_payload(keys) assert value == payload, f"\nОжидаемый результат: {value} " \ f"\nФактический результат: {payload}" return self
Для получения значения нужного параметра из респонса – добавим ещё один метод класса Api.
def get_payload(self, keys: list): """Получаем payload, переходя по ключам, возвращаем полученный payload""" response = self.response.json() payload = self.json_parser.find_json_vertex(response, keys) return payload
Для корректной работы метода get_payload добавим в наш “helper” модуль parser.py:
"""Модуль для парсинга данных""" from typing import Union def get_data(keys: Union[list, str], data: Union[dict, list]): """Получение полезной нагрузки по ключам, если нагрузки нет, возвращаем пустой dict""" body = data for key in keys: try: body = body[key] if body is None: return {} except KeyError: raise KeyError(f'Отсутствуют данные для ключа {key}') return body
Не забываем добавить новый модуль из «helper» в класс «Api»:
from helper.parser import get_data
Вы скорее всего уже заметили, что каждый метод класса «Api» возвращает self; чуть ниже увидим, почему так и насколько это удобно.
Основной класс готов, что дальше
Можно сказать, что самая большая и сложная работа к этому моменту уже проведена; «скелет» фреймворка сформирован. Осталось нарастить «мясо»:
-
класс-коннектор для тестируемого API с описанием методов;
-
файлы моделей – дата-классы для реквеста и респонса;
-
json-схемы респонса;
-
фикстуры;
И финальное – нужно будет написать сами тесты.
Приступим к класс-коннектору. Создадим в директории «api» директорию «reqres». В ней создадим файл «reqres_api.py» – собственно, наш коннектор к тестируемому API. Пропишем URL, Endpoint и методы взаимодействия с API.
Код нашего класса, на примере с post-запросом:
class ReqresApi(Api): """URl""" _URL = 'https://reqres.in' """Endpoint""" _ENDPOINT = '/api/users/' @allure.step('Обращение к create') def reqres_create(self, param_request_body: RequestCreateUserModel): return self.post(url=self._URL, endpoint=self._ENDPOINT, json_body=param_request_body.to_dict())
Теперь нужно создать дата-классы, в которых будет содержаться модель данных.
Сделаем новую директорию «model», внутри директории – файлы с моделями для наших данных.
Пример моделей данных для метода create:
"""Модели для create user""" from dataclasses import dataclass, asdict @dataclass class RequestCreateUserModel: """Класс для параметров request""" name: str job: str def to_dict(self): """преобразование в dict для отправки body""" return asdict(self) @dataclass class ResponseCreateUserModel: """Класс для параметров респонса""" name: str job: str last_name: str id: str created_at: str
Добавим использование моделей в класс «ReqresApi»:
from model.reqres.create_model import RequestCreateUserModel, ResponseCreateUserModel
Модели готовы и добавлены. Теперь самое время сделать десериализацию полученного респонса в объект данных, для использования в тестах.
Например, код метода десериализации для “single user” будет иметь следующий вид:
"""Собираем респонс в объект для последующего использования""" def deserialize_single_user(self): """для метода get (single user)""" payload = self.get_payload([]) return ResponseSingleUserModel(id=payload['data']['id'], email=payload['data']['email'], first_name=payload['data']['first_name'], last_name=payload['data']['last_name'], avatar=payload['data']['avatar'], url=payload['support']['url'], text=payload['support']['text'])
Если потребуется, сделаем по аналогии для остальных методов.
Следующим нашим шагом будет создание json-схем для проверки респонса. В корне проекта создаем директорию «schema», где будут находиться схемы ответов.
Схема для «single user»:
"""Схема для ReqRes API, single user""" schema = { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "email": { "type": "string" }, "first_name": { "type": "string" }, "last_name": { "type": "string" }, "avatar": { "type": "string" } }, "required": [ "id", "email", "first_name", "last_name", "avatar" ] }, "support": { "type": "object", "properties": { "url": { "type": "string" }, "text": { "type": "string" } }, "required": [ "url", "text" ] } }, "required": [ "data", "support" ] }
Следующий шаг – написать фикстуру, которая будет передавать в тесты экземпляр класс-коннектора «ReqresApi». В корне проекта создаем директорию «fixture».
Код фикстуры:
"""Фикстуры ReqRes API""" import pytest from api.reqres.reqres_api import ReqresApi @pytest.fixture(scope="function") def reqres_api() -> ReqresApi: """Коннект к ReqRes API""" return ReqresApi()
Всё готово – можно переходить к тестам!
Пишем тесты
В наш модуль «load.py» добавим метод для подгрузки данных непосредственно в тесты; будем параметризировать.
def load_data(path: str, test_data: str = 'data'): """Подгрузка из файла тестовых данных для параметризации тестов""" module = import_module(f"data.{path}") return getattr(module, test_data)
Добавим в корень проекта директорию «test», внутри – файл «test_single_user.py». Пример кода файла:
"""Тест кейс для ReqRes API, single user""" import allure import pytest from helper.load import load_data pytest_plugins = ["fixture.reqres_api"] pytestmark = [allure.parent_suite("reqres"), allure.suite("single_user")] @allure.title('Запрос получения данных пользователя с невалидным значением') @pytest.mark.parametrize(('user_id', 'expected_data'), load_data('single_user_data', 'not_valid_data')) def test_single_user_wo_parameters(reqres_api, user_id, expected_data): reqres_api.reqres_single_user(user_id).status_code_should_be(404).\ have_value_in_response_parameter([], expected_data) @allure.title('Запрос получения данных пользователя с валидным значением') @pytest.mark.parametrize(('user_id', 'expected_data'), load_data('single_user_data')) def test_single_user_valid_parameters(reqres_api, user_id, expected_data): reqres_api.reqres_single_user(user_id).status_code_should_be(200).\ json_schema_should_be_valid('single_user_schema').\ objects_should_be(expected_data, reqres_api.deserialize_single_user())
У нас получились два универсальных теста:
-
для невалидных параметров, с проверкой кода ответа и тела ответа;
-
для валидных значений: проверяем код ответа на соответствие json-схеме, десериализуем результат в объект, сравниваем его с эталонным (код остальных тестов можно посмотреть в репозитории).
Надеюсь, по коду тестов понятно, почему методы класса «Api» возвращают объект. Тут всё дело в том, что это позволяет довольно красиво и лаконично писать код теста, вызывая последовательно нужные методы класса и выполняя проверки.
Параметризацию тестов вывели в отдельный файл, чтобы не перегружать наш код тестовыми данными; в этом есть свои плюсы. При изменении тестовых данных их достаточно будет поправить только в одном месте – файле данных. При этом не проверять весь код и исправлять в самих тестах, что бывает проблематично.
Создадим файл данных для наших тестов. Добавим в корень проекта директорию «data»; внутри – файл «test_single_user.py».
Пример кода файла:
"""Дата-файл для тестирования КуйКуы API, single user""" # -*- coding: utf-8 -*- from model.reqres.single_user_model import ResponseSingleUserModel # эталонные модели данных для проверки в тестах # user_id = 2 user_id_2 = ResponseSingleUserModel(id=2, email='janet.weaver@reqres.in', first_name='Janet', last_name='Weaver', avatar='https://reqres.in/img/faces/2-image.jpg', url='https://reqres.in/#support-heading', text='To keep ReqRes free, contributions towards server costs are appreciated!') # user_id = 3 user_id_3 = ResponseSingleUserModel(id=3, email='emma.wong@reqres.in', first_name='Emma', last_name='Wong', avatar='https://reqres.in/img/faces/3-image.jpg', url='https://reqres.in/#support-heading', text='To keep ReqRes free, contributions towards server costs are appreciated!') # Валидные данные для тестов ('user_id', 'expected_data') data = ((2, user_id_2), (3, user_id_3)) # пустое тело ответа empty_data = {} # Невалидные данные для тестов ('user_id', 'expected_data') not_valid_data = ((129398274923874, empty_data), ('test', empty_data), ('роывора', empty_data))
Прогоняем тесты
Запустим наши тесты в консоли и посмотрим на полученный результат:

Тесты прошли, но получили не такой красивый отчёт, как можно было ожидать. Запустим тестовый прогон с формированием Allure-отчёта.

Тесты прошли, отчёт сформирован в папке allure_report. Откроем отчёт в локальном Allur.

Видим: было 17 тестов, все из них имеют статус “passed”.

На странице Suites наши тесты красиво разложены.
Плюс, для каждого теста имеем довольно информативный лог:

Сохранить: чеклист по созданию фреймворка для тестирования API
Итого, на пути создания нашего фреймворка мы прошли такие шаги:
-
завели директорию «api» и файл «api.py» – для работы с классом “Api”;
-
сделали файл requirements.txt – хранить список необходимых библиотек;
-
написали код отправки запросов и получения ответов;
-
добавили логирование
-
реквестов и респонсов
-
методов
-
-
завели ассерты
-
статус-код
-
json-схема
-
объекты
-
значения для определенного параметра
-
-
сделали класс для получения значения нужного параметра из респонса
-
в дополнение к основному классу завели
-
класс-коннектор для тестируемого API с описанием методов;
-
файлы моделей – дата-классы для реквеста и респонса;
-
json-схемы респонса;
-
фикстуры
-
-
написали тесты
-
для невалидных параметров, с проверкой кода ответа и тела ответа;
-
для валидных значений
-
-
прогнали тесты
-
по умолчанию
-
с формированием Allure-отчёта
-
Иметь под рукой чеклист – отправная точка. Кроме этого и остальных чеклистов, на пути начинающего автотестировщика будет ещё много всего: книги, статьи, видео, возможно какие-нибудь обучающие курсы; вопросы коллегам, обсуждения в тематических чатах и пабликах.
На мой взгляд залог успеха тут заключается в том, чтобы пробовать. Даже если не всё понятно, даже если не на все вопросы есть ответы – чем раньше начнёшь делать руками, тем раньше разберёшься. Пусть по шаблону прямым копированием шагов, пусть без глубокого понимания методологии – но делать руками как можно раньше.
Чеклист сработает как ожидалось – отлично, значит, статья была не зря. Получится найти или придумать более оптимальный чеклист – ещё лучше! Главное – пробовать.
Успехов вам в автотестировании!
ссылка на оригинал статьи https://habr.com/ru/companies/petrovich/articles/740050/
Добавить комментарий