Как стать автором
Обновить

Комментарии 17

Как правильно писать API авто тесты на Python

Заголовок с амбициями конечно :) Что такое "правильно", а что такое нет - это тот еще вопрос.

Когда мы пишем API автотесты, то нам хотелось бы, чтобы они отвечали требованиям…

И ниже перечислен список требований не к тестам, а к позитивному случаю конкретного теста. Вообще, требования к автотестам на АПИ это тема, которая может вытянуть на отдельную статью (я бы такую почитал с удовольствием), и должна затрагивать не так канкретные шаги в тест-кейсе как методологию выбора тест-кейсов и проверки в том или ином случае.

“Наш QA Automation ушел, поэтому теперь мы не можем даже запустить автотесты и непонятно, что в них происходит”. Это означает, что человек, написавший автотесты, писал их костыльно, как бы повышая свою ценность

Вывод явно не правильный. Если процессы построены так, что автотесты завязаны на одном человеке результат работы которого никто не ревьюит, то беда с процессами, а не с человеком. Ну и в написании кода очень много субъективного и результат может казаться одним правильным, а другим костыльным.

Еще один распространенный кейс - это когда новый QA Automation приходит на проект и сразу же хочет все переписать.

Да это старый как мир анекдот из жизни, что при смене разработчиков на любом проекте новые разрабы провозглашают все старое - устаревшим говнокодом и норовят все переписать. В автотестах все точно так же.

Requirements

Мне кажется, что в современном мире ставить зависимости пипом и без указания версий это моветон. Я бы поэтри использовал или какую другую альтернативу даже в автотестах.

Если конкретно по зависимостям, то не очень понял, почему бы джейсонки не валидировать пайдантиоком. Кстати, дотенв можно отдельно не ставить, а поставить его сразу с пайдантиком (pip install pydantic[dotenv]). За httpx лайкосик. А кстати, какие генераоторы отчетов бывают кроме алюра? Я когда-то его юзал и меня напрягало, что он не умеет статику генерить чтоб прилеплять её как артефакт (хотя может я просто сам себе злой буратино и просто не понял как это делать). Ну и в целом я не ощутил восторга от его использования.

Библиотека pydantic служит для реализации “строгой типизации” в python.

Питон и так язык со строгой типизацией. Можно здесь почитать, что такое строгая типизация и понять при чем тут питон https://habr.com/ru/post/161205/. Пайдантик, строго говоря, в современном мире используется для каста значений к указанным типам и валидации.

В качестве альтернативы pydantic можно взять библиотеку models-manager

Теплое и мягкое. Либа для моделей и ORM. В качестве альтернативы я бы рассматривал только мармелоу, но это вкусовщина и “ну такое”. Вообще, если уж коснуться темы походов из автотестов в базу, то я бы наверное использовал “сырые” запросы, но тут тоже добрая доля вкусовщины.

class Settings(BaseSettings):

Три точки не обязатально писать. Для работы с префиксами в переменных окружения в конфиг можно указать специальный параметр. И удивительно, что вы объявляете переменную в глобальном скоупе, а не засовываете её сразу в фикстуру. Кстати, в доке пайдантика настройки получают из кешированной функции и код из-за этого чуть чище кмк.

models\questions.py

Напрашивается наследование одной модели от другой и непонятно что делает в названии класса слово default. То есть:

class DefaultQuestion(UpdateQuestion)

И я бы все-таки убрал default_factory из моделей и написал простую функцию фабрику (ну что-то в духе def make_question_model_with_random_data()) для генерации моделей с рандомными данными. Из разряда вкусовщины: люблю страшно фикстуры в пайтесте и генераторы рандомных данных в них бы упаковал завернум в алюровский декоратор с шагом, но это ну такое. Вообще, рандомные данные в тесты я не любитель пихать, но иногда без этого, конечно же не обойтись.

JSON схема генерируется автоматически на основе модели.

Валидируйте без схемы, а сразу моделью пайдантика.

base\client.py

По поводу if (not auth.auth_token) and (not auth.user): непонятно почему бы это не валидировать в самой модели. И в тестах прям напрашивается assert. NotImplementedError явно не к месту. Ну или на крайняк ValueError.

Поэтому не забудем добавить нашу API фикстуру в pytest_plugins

Прочитал на пару раз и так и не понял зачем мы её добавляем в pytest_plugins

ps: Далее по диагонали читал. За старание пять, а по содержимому натянутая 4ка с минусом, как по мне, и не в последнюю очередь из-за того, что заголовок намекает на вычищенность подхода, а на мой взгляд есть еще над чем работать. Готов подискутировать по своему комменту если считаете, что где-то я не прав. Про упоминание своих реп - надо бы четко обозначать, что вот сомтрите так и эдак вот моя репа и в ней то-то и то-то, а лучше даже отдельным постом с описанием того, что в либе есть и обзором плюсов и минусов своего решения.

  1. Без кометариев

  2. Я написал только те требования к API, которые нужны лично мне или большинству для написания API авто тестов. И даже специально написал "Для меня требования выше являются базой, ваши же требования могут быть другие в зависимости от продукта. "

  3. Да, в этом и проблема. Люди учатся по материалам, которые заведомо учат их неправильному. Далее компания становится пелйграундом для такого человека, пока он не попадет в команду, где его чему-то научат. У некоторых это не случается никогда

  4. Да, наверное и в разработке и везде есть такое. Но, как показывает моя практика, с такими людьми можно успешно бороться и пускать их пыл все переписать в нужное русло

  5. На github есть все зависимости и версии. Я не стал заморачиваться с poetry, моя главная задача это авто тесты. Статья и так получилась очень большая. Есть много репортеров allure, pytest-html, report-portal, junit, но тут даже речь не о репортере, сейчас почти все +- не бедные компании используют Allure TestOPS, в нем есть все, от коллаборации Manual + Automation инженеров, до аналитики и статистики, короче топовая штука.

  6. В python нет строгой типизации. У python динамическая строгая типизация https://ru.wikipedia.org/wiki/Python, это означает, что одна и так же переменная может иметь разный тип в процессе выполнения программы. Где используется Pydantic тоже субъективно, где только не используется, некоторые даже умудряются пихать его в Django.

  7. Без комментариев

  8. Если положить настройки в фикстуру, то потом фикстура будет доступна только в скоупе тестов. То есть если вы захотите добавить какой-либо скрипт, который например чистит стенд или создает окружение или еще что-то, то фикстура тут будет не доступна. Это не гибкое решение. И даже если положить настройки в фикстуру, то потом придется эти настройки передавать в каждую функцию/класс и получится огромная вложенность. По поводу елипсов, их даже в документации используют https://docs.pydantic.dev/usage/settings/, но я знаю, что не обязательно

  9. Наследовать тут не вариант т.к. типы у объектов будут разные. Как в pydantic сделать все поля опциональными я не нашел, по крайней мере официального решения, есть только костыли и хаки. Да, можно через функцию сделать, например get_random, но я не вижу проблем с default_factory.

  10. Не совсем понял, в чем тут проблема

  11. Не люблю использовать root_validator, но можно валидировать и в нем. В тестах используется expect, дефолтный assert хоть и хорош с pytest-ом, но он не даст нам allure.step

2

Проверки должны быть полными, то есть мы должны проверить статус код ответа, данные в теле ответа, провалидировать JSON схему;

Если вы хотите валидировать жсон схему - используйте отдельную схему от той, что вы используете в коде явно. Иначе вы не отловите кейсы изменения схемы, которые, как я понимаю, должны падать. Иначе тест сводится к "в этом эндпоинте используется схема с этим именем"

Автотесты должны быть документированными и поддерживаемыми. Чтобы автотесты мог читать и писать не только QA Automation, но и разработчик;

Документирование автотестов выглядит как что-то ненужное. Максимум, что можно сказать о документированности - из имени метода должно быть понятно что делает этот тест или к чему призван, из за чего написан.

Хотелось бы, чтобы JSON схема и тестовые данные генерировались автоматически на основе документации;

Посмотрите в сторону faker и подобных инструментов. Наверняка есть что то, что генерирует данные на основе пидантик схемы

Отчет должен быть читабельным, содержав в себе информацию о ссылках, заголовках, параметрах, с возможностью прикреплять какие-то логи.

Выглядит как что то совсем излишнее. Если это, конечно, не касается end2end тестов

3 Это всё ещё означает, что процессы плохо выстроены именно у вас

6 Вы сначала говорите, что в python нет строгой типизации и сразу же себя опровергаете. Не надо так :)

8 Если я правильно понял комментатора выше, имелось ввиду не настройки в фикстуру, а сделать функцию с кэшем получения настроек. Как в доке фастапи. Если мы говорим опять же про фастапи, то можно настройки подменять через Depends. Фикстуру, к слову, можно прикинуть во все тесты, явно её не передавая из за магии autouse=True.

10 Модель Authentication вполне может в валидацию того, что вы написали в клиенте. Мне кажется, комментатор выше именно это имел ввиду.

  1. Вот это кринж. Подумайте логически, если на бэке схема изменится, то со стороны авто тестов, она будет прежней, мы получим разницу и фейленный тест.

  2. Под документацией имелось ввиду, типизация объектов по максимуму

  3. Меня все устраивает, вы и комментатор выше, можете посмотреть, если вам так это нужно. Я лишь описал концепцию, думаю смышленые люди разберутся, что им нужно

  4. Мне вот интересно, как такие умники дебажат авто тесты? Если в отчете только абстрактная информация, без какой-то конкретики

  5. Еще немного кринжа. Строгая типизация и динамическая строгая типизация это разные вещи, советую изучить этот вопрос

  6. Еще немножко кринжа, оставлю это без комментариев

  7. Да, что-то можно было бы вынести в модели

    Эта статья была написана для людей, которые хотят найти верный подход для написания автотестов. Все выше почему-то воспринимаю это, как туториал, что ошибочно. Если вас не устраивают какие-то моменты, то это ваши проблемы, задача статьи только в том, чтобы описать концепцию. Описать все подходы, использовать все супер модные либы, отполировать все до идеала, не входило в мои планы

1 В этом и суть. Иначе такие тесты кроме имени схемы ничего не валидируют.

4 Строить тесты таким образом, чтобы они документировали сами себя? Прям как код? Естественно, такое возможно не всегда, но очень часто этого достаточно.

5 Есть строгая/нестрогая типизация. Также есть статическая/динамическая. То, что вы, говоря о строгой типизации, имеете ввиду строгую статическую, желательно упоминать явно.

  1. Мне кажется мы с вами про разное говорим

  2. Я бы сказал так, что документации в виде типов будет достаточно для понимания. Но еще бы хотелось, чтобы на основе авто тестов генерировалась тестовая документация, то есть тест кейсы. Но тут уже зависит от процессов, у кого-то совсем нет документации, только маленькие чек-листы, сначала пишутся авто тесты, потом автотесты генерируют документацию, при этом документация всегда актуальна т.к. мы чиним тесты. Кто-то на оборот, сначала пишет тест кейсы, потом на основе них делает автотесты. Лично мне нравится первый подход

  3. Я думаю, все поняли о чем речь, в python все же нет такой типизации, как например, в C# или Java. Даже если мы указываем явно тип переменной int, в python ничего не мешает нам потом сделать эту переменную строкой. Я как раз это имел ввиду, и поэтому сказал, что pydantic якобы реализует "строгую типизацию", т.к. он будет ругаться, на все, что не подходит под нужный тип, специально даже выделил кавычками, думал меня поймут, но нет. Короче кому надо, тот будет использовать и не так важно, как это называется

Вы сделали прекрасный базис для тестирования rest-api сервисов, к сожалению api бывают еще и всякие разнообразные RPC, мир живет не только на http, думаю поэтому статью минусят.
По моему опыту большая часть проблем с авто-тестами немножко все ж не на уровне базового слоя, голый pytest + request достаточно читабельный, а вот уровень тест-дизайна страдает по максимуму - даже при помощи вашего фреймворка можно будет понаписать лапши, которая не бьется ни по одной из техник https://highload.today/blogs/8-tehnik-test-dizajna-s-primerami/#8
Если честно, мне кажется ваш цикл статей пытается решить организационные вопросы менеджмента разработки небольших компаний с помощью кода вместо использования софтскилов.
Проблема подхода в том, что без оглядки изобретается велосипед, который уже проработан у корпоратов, я помню очень похожий слой авто-тестов предлагал оператор МТС, но их авто-тесты жили в парадигме TDD и писались до создания сервисов - на зафейленный тест тут же заводился автоматом баг и разрабы писали код сервиса под тесты

Мне кажется это каким-то бредом, приходить читать статью где тестируют rest api и минусовать ее за то, что нет ничего кроме http протокола

Это равно тому, если бы вы пришли в магазин "одежа" и сказали, а какого фига тут нет капусты?

Я описал лишь концепцию, далее есть множество путей развития

Мне кажется, более реальная причина хейта кроется в российском менталитете. Благодарный читатель будет извелекать 150% информации из работы любого автора, инфантил же, скажет, что одна вода и вообще автор дурак

Мне как junior automation статья очень понравилась, много нового для себя открыл, спасибо!

Рад слышать, что статья оказалась полезна вам!)

Спасибо автору за сэкономленное время, утащил себе.

По поводу комментариев выше не понимаю к чему они были написаны, напишите свою статью в конце концов. Если вы на 100% уверены что так реализовывать не нужно - зачем читать статью целиком, тратить свои минуты на критику (в общем то бесполезную, не увидел супер конструктивных замечаний, почему надо делать вот так и никак иначе)? Не ракету в конце концов проектировать собираемся, цена ошибки минимальна.

Приятно слышать, что кому-то материал оказался полезен)

По поводу комментариев, абсолютно с вами согласен. Критиковать кого-то намного проще, чем похвалить, для критики или хейта даже слова легче подобрать, нежели для положительного фидбэка

Спасибо за статью, открыл для себя много нового. Постарался написать пару тестов на рабочем проекте следуя описанному выше подходу, хочу поделиться впечатлениями и уточнить пару вещей.
1) Схему удобнее валидировать пытаясь переложить ответ в ожидаемую модель например методом Модель.parse_obj(ответ). Если же использовать способ из статьи, то регулярно будут ошибки для полей, у которых допустимы несколько типов. Может я чего-то не понимаю, но полученная из модели схема всегда содержит в себе только один тип для каждого поля.
2) У нас порядка 40+ микросервисов, у каждого десяток-другой эндпоинтов, соответственно схем будет довольно много, не особо понимаю как это добро структурировать. Сервис 1 вполне вероятно внутри своего ответа использует модель сервиса 2, значит надо либо импортировать эту модель, либо описывать ее заново. В первом случае надо вспомнить что такая модель уже где то описана, что довольно сложно учитывая их общее количество. Во втором случае есть риск накопипастить кучу дублирующего кода. Не понимаю как лучше поступить, интересно послушать про "боевой" опыт поддержки моделей.
3) При реализации HTTP методов (для работы с questions) в аргументах указана ожидаемая модель из которой будет формироваться тело запроса, это круто. Но как получить ожидаемую модель тела ответа? Опять же, при большом количестве моделей легко запутаться. Как будто бы напрашивается еще один слой, переделывающий Response в конкретную модель. Что-то похожее сделано в методе create_question (который из работы с questions). Однако если мы получим ошибку в ответе (а значит другую структуру ответа), то метод упадет и в отчете это будет не особо читаемо. Получается какой-то промежуточный слой между драйвером и тестовой логикой, не понятно нужен ли он вообще и где его реализовать, если он нужен.
Возможно эту проблему закрывает QjestionDict. В статье 5 тестов, в 4 из них словарь с ответом аннотируется типом QjestionDict. Это удобно, когда все ручки выдают один ответ, но что делать, когда у каждой ручки своя схема?

С pydantic'ом познакомился недавно, поэтому возможно я просто что-то не понимаю. Буду рад ответам на вопросы.

Рад, что статья оказалась полезной. Давайте по вопросам:

  1. Да, такой вариант тоже возможен Модель.parse_obj(ответ), вполне. Для моделей у которых несколько разных типов полей можно использовать объединения. Например у нас есть JSON объект, в котором есть поля {"id": 1, "state": 1}. Поле "state" может быть строкой, числом, списком из чисел. Тогда модель будет выглядеть так


    Для python 3.10+ эта запись может быть другой, например state: str | int | list[int]. В итоге при генерации схемы мы получим поле state в таком формате

    В данном случае поле state является anyOf string, integer, array + items integer. То есть все эти JSON объекты будут валидны


    Таким образом, мы можем описывать разные типы полей. Надеюсь я правильно понял ваш вопрос

  2. Ну тут нужно смотреть индивидуально ваш продукт. Не смогу сказать точно, как это сделать именно для вашего проекта, т.к. мне нужно будет знать много пунктов. Вот, что можно предпринять:
    1. Выбрать правильную структуру для моделей. Например у нас есть сервисы users, grading, payments
    Тогда раскладываем модели так:
    models/users/user.py
    models/users/auth.py
    models/users/emails.py

    models/grading/score.py
    models/grading/grade_scale.py

    models/payments/orders.py
    models/payments/items.py

    Это должно решить проблему с запутанностью. Понятно, что у вас скорее всего структура будет намного сложнее, но суть к остается та же. Если раскидать модели по правильной структуре, то мы сможем всегда знать, где находится нужная нам модель.

    2. Если же у вас ситуация по типу, когда внутри одного и того же объекта могут быть разные модели, например


    То в таком случае, чтобы не писать каждый раз новую модель и не дублировать код, можно воспользоваться Generic моделью

    В таком случае T будет динамически заменяться на тот тип, который вы передадите. Это поможет избежать дублирования кода, если внутри какой-то общей/глобальной модели используются разные модели из других сервисов.

  3. Возможно дополнительный слой нужен. В примере с questions я писал два метода

    create_question_api
    create_question

    Внутри create_question хорошо было бы добавить проверку, черед тем, как прокидывать response.json() внутрь модели, то есть:


    Тогда в случае ошибки мы упадем раньше, чем попадем в модель.

    Можно поступить по другому, в pydantic есть такая киллер штука, как ValidationError, с помощью нее, мы можем сформировать очень даже читабельное и понятное сообщение об ошибке в отчете. Например нам из API пришел пустой объект, тогда мы можем попробовать спарсить его и если что-то не так, то кинуть сообщение об ошибке


    Фишка тут в error.json(), эта ошибка будет выглядеть примерно так

    Что +- можно прочитать и понять, что именно было не так. Дальше уже вопрос в том, как вы это будете применять. Можете написать обертку, которая сначала будет пытаться запихнуть json в модель, если не получится то выбросит ошибку в json формате. JSON формат ошибки можно настроить или отформатировать, как вам удобно

Спасибо за подробный ответ!
Я наверное как то криво описывал модель в pydantic, для атрибутов пробовал указывать так же как и вы в пункте 1, но схема генерилась иначе... Ну да ладно.
Я правильно понимаю, что вы выстраиваете свой тестовый фреймворк похожим образом?
Очень интересно узнать сколько у вас API тестов и как тяжело их поддерживать. Закрадываются мысли вроде "а не проще ли забить на эти модели и оперировать в тестах json'ом, полученным из экземпляра Response".
Валидацию схемы же оставить на jsonschema, саму схему генерить на любом удобном сайте по имеющемуся ответу и складывать в одну папочку в репе. Вроде так проще и быстрее, даже коллеги не жалуются на ревью. Поэтому и спрашиваю, быть может вы научены горьким опытом и эмпирически пришли к "паттерну", описанному в статье? Наверняка вы сначала писали тесты иначе, потом столкнулись с какой-то проблемой, которую решили описанным выше образом. Что это была за проблема?

Да, тесты пишем именно таким образом. Отличие в том, что сама структура тестов более сложная, в статье много было опущено, + мы используем async await для всех тестов API и UI.

Тестов достаточно много, 3000+ API тестов. Поддерживать с таким подходом просто, гораздо сложнее поддерживать было ранее.

Я пришел к данному подходу спустя несколько лет разных проектов, разных компаний, разных команд (от 1 QA Automation на 1 проект, до 5-7 QA Automation в одной команде). Куда бы не попадал, везде были похожие проблемы, похожие решения. Потом я изучал очень много данных по автоматизации тестирования, это были, как русскоязычные, так и англоязычные ресурсы. После нескольких лет практики, я вывел одну важную вещь: даже если компания крупная, даже если там большая команда QA Automation и большой продукт, это не мешает QA Automation инженерам изобретать велосипеды. Ну или в компании может быть главный "велосепедист") Такого человека еще в шутку называют "Хранитель говнокода") Обычно это QA Automation, который пришел в команду раньше всех и почти весь фреймворк написал он, ни в коем случае не хочу никого оскорбить, просто так бывает

1. Сначала мне хотелось решить проблему с типизацией в автотестах. Изначально все выглядело так

def create_something_api(payload: dict, user: dict = None) -> Response:
...

Где-то писалось так:

class CreateSomethingPayload(dict):

"""Тут мб какое-то описание"""
pass

def create_something_api(payload: CreateSomethingPayload) -> Response:
...

Но мне ни первая, ни вторая записи не нравились. + В команде менялись люди, кто-то уходил на другой проект, потом брали новых QA Automation, иногда это были даже джуны. Часто случалось так, что тот человек, который писал этот код уходил из компании и уже было сложно понять, что же там написано. Усложнялось тем, что не на всех проектах был сваггер и документации по API не было, как правило это компании стартапы, которых сейчас не мало. Даже если сваггер был, то ходить каждый раз в сваггер было не удобно + шанс запутаться был только выше.

Потом на помощь пришли dataclass-ы. Которые +- решили проблему с типизацией. Но сразу же появились проблемы с тем, чтобы конвертировать модель dataclass-а в json/dict, генерировть схему. Тут на помощь пришла либа dataclasses-json, которая делает +- тоже самое что и pydantic, где-то криво-косо, но на тот момент было хорошо. Но в текущих реалиях, я 100% отдаю голос за pydantic, у него намного больше фичей, чем у dataclasses-json.


2. Еще была проблема с json schema. Ее хардкодили, автоматически схему никто не генерировал. Мне тоже это не нравилось т.к. в проекте было ну очень много enum-мов, со всякими статусами, кодами, было много сущностей, у которых было поле type, которое было enum-мом. Обычные сайты по типу generate json schema online могут сгенерировать схему для простого объекта, но они не учитывают что какое-то поле не просто striing, а 'enum': ['started', 'finished'].

Из-за хардкода схемы приходилось делать в два раза больше работы. Если разработчики обновили type какой-то сущности, то нам нужно было обновить enum в автотестах + переписать схему руками, ведь online генератор в этом не поможет. + Иногда люди криво писали схему и совсем забывали, что поле не просто integer, а какой-то конкретный enum.

Эта проблема решилась вместе с dataclass-ми + либа dataclasses-json. Всю хардкоженную схему убрали. Теперь при изменении enum-ма схема автоматически обновлялась, т.к. генерировалась автоматически.

Наверное тут можно было бы сказать, что эту проблему можно было бы решить автоматически через генерацию клиента на основе swagger. Да, согласен, если у вас есть ресурсы и время, на написание скриптов для генерации клиента. Но на практике именно для python я не видел таких скриптов, только для Java.

3. Лично мой выбор, это отказ от requests в пользу httpx. Потому что сейчас все пишем через async await, ну и философия библиотеки httpx мне больше близка. Но requests тоже очень даже хорошая либа

И снова спасибо за подробный ответ!
Мы только начинаем писать апи тесты на проекте, хочется на берегу выработать какой-то подход, который в дальнейшем позволит поддерживать тесты без лишней боли. Возможно через годик-другой смогу дать свой фидбек =)

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории