Comments 35
У вас в примере упоминается Swagger, который позволяет генерировать клиента в том числе и на Python. Идея не писать весь этот бойлер-плейт руками выглядит довольно привлекательно. Что бы вы сказали "за" и "против" такого подхода?
Интересно глянуть, что он генерирует. Вообще с нормальным клиентом не вижу смысла использовать что-то генерёное. Накидать несколько ендпоинтов - не то, чтобы самое трудозатратное в автоматизации. Боди надо будет или самому вносить или проверять, хэдеры тоже. Как оно будет подтягивать изменения - непонятно, отдаём стабильность тестов на откуп команде бэкенда. Я бы не стал на серьёзном проекте таким заниматься, особенно если есть CI/CD
Мое мнение, что такие автогенераторы клиентов в основном подходят для пет проектов, либо я не встречал проекты, где их можно было использовать, так как приходится писать тонны вспомогательного кода рядом. Так же я не встречал сваггер, который находится в актуальном состоянии, поэтому я против такого подхода (исходя из своего опыта)
Я использую https://github.com/p1c2u/openapi-core на бекенде для конфигурации routes, парсинга запросов и валидации ответов. Это вынуждает поддерживать swagger в актуальном состоянии, иначе приложение просто не заработает. Вообще swagger служит контрактом между командами, поэтому должен быть всегда актуален.
Да, абсолютно верно, сваггер всегда должен быть актуальным.
На своих проектах я не могу использовать такие генераторы клиентов, мне все равно пришлось бы руками много писать
к сожалению генерировать api клиент из сваггера для тестов не очень получается, т.к. зачастую в тестах необходимо что-то специфическое (добавить/удалить поле и т.п.), поэтому проще писать своего клиент, но вот использовать сваггер для валидации ответа прям отлично
Это уже офтопик, но хотелось бы позицию до конца прояснить: а к использованию ORM в автотестах вы как относитесь?
Все дело в том как конкретно применяют Swagger. В библиотеках на Python принята идеология "code first", когда схема генерируется по уже готовому API (например Django DRF) и тогда возникает куча проблем с несовместимой схемой, на это забивают и в конечном счете от сваггера не остается ничего.
Если же использовать логику "schema first", то выясняется, что прям хороших генераторов сервера для Python нет. А то что есть не дотягивает до уровня Fast API, например.
В других языках schema first может лучше работать, а может и хуже.
response.response.get
Зачем два раза response? Такие тавтологии обычно являются признаком наличия проблем с дизайном (возможно где-то появился лишний слой абстракции).
вся архитектура стала гибкой и легко поддается рефакторингу
Ценой увеличения порога входа, сокрытия важных деталей и необходимости поддерживать кастомный фреймворк. Вообще история напомнила классическую эволюцию программиста.
В идеале код теста должен выглядеть как спецификация, глядя на которую становится сразу понятно куда и какие данные отправляются и что ожидается в результате. В этом смысле исходный вариант, где все подробности торчат наружу, мне нравится больше.
Скажем, первый шаг с валидацией через схему мне показался оправданным. А вот все последующие - лютый оверкилл какой-то.
Тут забавный парадокс. Поддерживать эти обертки над обертками над обертками более или менее удобно, когда всё более или менее стабильно в плане используемых фреймворков. Однако зачем писать обертки над тем, что и так стабильно? А если на проекте всё постоянно меняется, то при наличии этих абстракций, количество мест где надо что-то где-то поправить только увеличится.
В общем, уверен на каких-то больших масштабах указанный подход имеет место быть, но в маленьком проекте - это прям паттерны головного мозга.
Как только ваши тесты переваливают за условную цифру в 100 тестов, то подход "пишем в лоб" и "все подробности торчат наружу" становится губительным для проекта тестов, поддерживать такой код становится невозможно.
А если на проекте всё постоянно меняется, то при наличии этих абстракций, количество мест где надо что-то где-то поправить только увеличится.
Ровно наоборот и происходит)
за условную цифру в 100 тестов
Я бы увеличил эту условную цифру хотя бы до 1000. Иначе, как правильно заметили выше, все эти абстракции над абстракциями будут только мешать ещё не стабилизировавшемуся приложению. А скрывать requests, из за страха его будущей замены, я бы не стал ещё дольше — бессмысленная трата ресурсов.
Ровно наоборот и происходит)
Ровно наоборот происходит только при удачно выбранных абстракциях. Но на такое способен только очень скиллованный специалист. Но тогда непонятно на кого рассчитана ваша статья, потому как тут же вы поясняете что такое Faker и как сложно настроить логгирование в python/
Иначе, как правильно заметили выше, все эти абстракции над абстракциями будут только мешать ещё не стабилизировавшемуся
Это никак не связано, на каком этапе проект, стабилизировалось или нет, а вот код в лоб практически сразу становится сложно поддерживать. Код, который я показал, пытается решить эту проблему на старте проекта
А скрывать requests, из за страха его будущей замены, я бы не стал ещё дольше — бессмысленная трата ресурсов.
requests скрывается не из за "страха его будущей замены", условная будущая замена это пример, для чего мы это делаем, а не "молча выделим новую абстракцию". Вынести запросы в отдельный файл (кстати, почему это бессмысленная трата ресурсов?) это инверсия зависимостей.
код в лоб практически сразу становится сложно поддерживать
Такие абстракции каждый пишет по своему и каждый раз это как новый фреймворк(может быть хороший), который надо изучить, но по которому нет документации
условная будущая замена это пример, для чего мы это делаем, а не «молча выделим новую абстракцию»
Так requests надо скрывать или нет? И зачем?
Такие абстракции каждый пишет по своему и каждый раз это как новый фреймворк(может быть хороший), который надо изучить, но по которому нет документации
Из моей практики происходит наоборот. За счет абстракций код хорошо читается и новый человек в команда достаточно быстро начинает писать тесты, смотря на верхнеуровневые примеры, постепенно погружаясь ниже.
П.C. документация у нас есть и она обязательна)
Так requests надо скрывать или нет? И зачем?
Задам встречный вопрос - для чего в PageObject используют base_page? Какую проблему это решает? Или можно в пейджах оставить методы селениума/плейврайта?
Из моей практики происходит наоборот. За счет абстракций код хорошо читается и новый человек в команда достаточно быстро начинает писать тесты
Всё равно не пойму зачем выносить стабильный и всем понятный requests куда то и заменять его каким то custom_client.
По итоговому тесту есть и другие замечания. Во первых именования — уже отмеченный response.response и почему body получается из объекта RegisterUser — магия сплошная, которой новый человек ещё должен будет овладеть. Ну и проверка message на мой взгяд лишняя — лучше http код проверять.
Всё равно не пойму зачем выносить стабильный и всем понятный requests куда то и заменять его каким то custom_client.
На самом деле можно и не выносить и оставить как есть. И во многих примерах/документациях так. Но если по правильному - то выносить. Опять же сошлюсь на PO, так как он более менее знаком большинству - base_page для чего он? Ведь есть понятный и стабильный selenium. И когда вы ответите на этот вопрос, то будет понятен финт с requests.
Во первых именования — уже отмеченный response.response
Да, соглашусь, можно было более четче разделить именование, что бы не было путаницы
и почему body получается из объекта RegisterUser — магия сплошная
Не совсем понял ваш вопрос, класс RegisterUser отвечает за рандом данных для отправки регистрации пользователя. Да, его можно и нужно переписать и улучшить, но статья не про это.
Ну и проверка message на мой взгяд лишняя — лучше http код проверять.
Тут с вами сильно не соглашусь, проверка респонса обязательна
Тут с вами сильно не соглашусь, проверка респонса обязательна
вы message на строгое соответствие проверяете. Например в мультиязычном приложении, такие сообщения иногда выносятся в отдельные файлы, доступные для правки не только программистами. Не думаю что тесты должны падать, только из за того что переводчик решил изменить формулировку сообщения.
У вас там на selenium в статье ссылка битая. И отвечать вопросом на вопрос не только не очень вежливо, но наталкивает на мысль о том «знаете ли вы ответ». Пока что я понял что так нужно делать потому что нужно
вы message на строгое соответствие проверяете. Например в мультиязычном приложении, такие сообщения иногда выносятся в отдельные файлы, доступные для правки не только программистами. Не думаю что тесты должны падать, только из за того что переводчик решил изменить формулировку сообщения.
Это очень специфичная ситуация, когда в респонсе ответы мультиязычные, такое бывает не часто, это не может быть причиной что бы не проверять текстовки в ответе.
А проверять нужно и падать обязательно, так как одно дело, когда разработчик поменяет текстовику, а другое, когда вместо текста у вас пролезет стектрейс Java/scala/js и вы сможете отловить ошибку заранее.
для чего в PageObject используют base_page?
https://wiki.c2.com/?GodClass
Какую проблему это решает?
Предположу, что даёт возможность команде AQA поиграть в программистов-архитекторов, а за одно помогает раздувать сроки, штат и бюджет на ровном месте.
Как тиражировать такое решение на разные команды/проекты, неужели вы предлагаете каждой писать свои обёртки поверх requests?
Я не думаю, что 10 строчек кода раздует проект )
Разработчики, при написании кода , используют паттерны, подходы, mvc и так далее. И делают они это не для того, что бы раздуть сроки, а для читаемого, гибкого кода и возможности быстро и безболезненно делать рефакторинг (да, это не гарантия, но всё же). Чем aqa “хуже”? Почему автотестер должен писать код в лоб, не думаю наперёд ?
Ничем не хуже, это такой же разработчик. Правильный построенный код (фронт, бэк, тесты) упрощает поддержку кода. Обычно команды тестирования, которые не могут это сделать/не хватает опыта и пишут «как есть», приводят проект к раздуванию и удорожанию (так как такие тесты легче выкинуть, чем поддерживать).
Проблема в том, что нужно писать тесты на тесты тогда. Чем больше наворочено в тестах, тем сложнее их читать и тем больше возникает нюансов при из написании/поддержке.
Ну т.е. это достаточно холиварная тема.
По поводу переиспользования тестов с помощью фейкера - ну частично решает проблему, но гарантий уникальности не даёт. А для переиспользования тестов существуют фикстуры, которые сначала создают для теста нужные сущности а после себя чистят. После теста должен остаться только артефакт - отчёт или лог. А у вас база данных будет в куче пользователей.
По поводу переиспользования тестов с помощью фейкера - ну частично решает проблему, но гарантий уникальности не даёт.
Если нужна уникальность, то обычно делают так
def random_email() -> str:
"""
Get random email
"""
timestamp = pendulum.now().int_timestamp
return f"{timestamp}_{fake.email()}"
А для переиспользования тестов существуют фикстуры, которые сначала создают для теста нужные сущности а после себя чистят. После теста должен остаться только артефакт - отчёт или лог. А у вас база данных будет в куче пользователей.
В самом начале я написал, что пишем без фикстур) Так как это бы только бы отвлекало.
На самом деле фикстуры никому ничего не должны и как себя вести - решаете вы на своем проекте. Я соглашусь, что по правилам хорошего тона - пришел, протестировал и после себя убрал. Но иногда полезно наполнить вашу бд пользователями.
В любом случаи, статья не про фикстуры, поэтому эта часть работы намеренно опускается.
Асерты без строки, если свалится будет тупо питоновская ошибка, нужно копаться и вспоминать что там упало.
Тестовый класс и его метод без докстринги ( то что там урл написан ясности что в нём проверяется не даёт). Чтобы понять, что делает тест, надо читать код, лезть под капот, тратить время.
Инициализация клиента внутри тестового класса - не знаю, может и есть в этом что-то, но что если мы напишем другой клиент? Или у нас есть экземпляры клиента для разных случаев? И зачем вообще каждому тестовому классу свой клиент?
Файл с клиентом requests.py - зачем эта путаница? ну пусть бы был custom_client или ещё какой, но как основную либу для запросов то зачем называть?
Тестовая функция не принимает аргументов, значит нельзя её параметризировать, значит отпадает здоровый кусок функционала, который нам предоставляет PyTest. Если мы захотим сделать отрицательный тест, нам надо будет либо копипастить тестовую функцию, либо выносить параметры, чтобы их можно было перебирать через pytest.mark.parametrize
Когда дело дойдёт до запуска, эти тесты надо будет либо списком вызывать, либо по файлам распихивать, что одно что другое неудобно. Поэтому существую маркеры.
Так как у нас есть положительные и негативные тесты ...
Не увидел отрицательных тестов
Знаю, что мои замечания больше про большой проект, но всё же когда показываешь пример, надо или говорить что это автотест на один эндпоинт или уже показывать как масштабировать. Этот код не подходит для серьёзного проекта ( хотя бы потому что не используется питест)
Хоть автор и пишет, что он абстрагируется, толку от выноса импорта особо нет, поменять импорт при переходе на другую библиотеку - самое малое, что нужно будет сделать. Хочешь сделать на асинхронной либе - ну сделай сразу (хотя зачем, если есть xdist - непонятно).
Сразу извиняюсь если слишком токсично, у меня нет 17 лет опыта автоматизации, но если бы статья называлась "как улучшить плохой автотест" или вроде того, не было бы замечаний, а так не удержался.
Так же хотелось бы отметить, что код ниже про архитектуру, некоторые вещи я намерено упрощаю, что бы не затягивать.
Основная цель статьи это вопрос архитектуры
Асерты без строки, если свалится будет тупо питоновская ошибка, нужно копаться и вспоминать что там упало.
Да, лучше дописывать строку, что бы коллеге было больше информации, если упадем, то мы в любом случае словим "питоновские ошибки", но с пайтестам нам подсветят дифф
E AssertionError: assert 'User created successfully.' == 'User created successfully.1'
E - User created successfully.1
E ? -
E + User created successfully.
Инициализация клиента внутри тестового класса - не знаю, может и есть в этом что-то, но что если мы напишем другой клиент? Или у нас есть экземпляры клиента для разных случаев? И зачем вообще каждому тестовому классу свой клиент?
Где инициализировать клиента это второй вопрос, это не было целью статьи
Тестовый класс и его метод без докстринги ( то что там урл написан ясности что в нём проверяется не даёт). Чтобы понять, что делает тест, надо читать код, лезть под капот, тратить время.
Можно сделать так
def test_registration(self):
"""
Steps
1. Try to register new user
2. Check, that status code is 201
3. Check response
"""
body = RegisterUser.random()
response = Register(url=URL).register_user(body=body, schema=valid_schema)
assert response.status == 201
assert response.response.get('message') == 'User created successfully.'
assert response.response.get('uuid')
В статье рассматривался один тест, сценарий его оговаривался, поэтому код был дан без докстрингов, но в следующий раз учту, спасибо
Тестовая функция не принимает аргументов, значит нельзя её параметризировать, значит отпадает здоровый кусок функционала, который нам предоставляет PyTest. Если мы захотим сделать отрицательный тест, нам надо будет либо копипастить тестовую функцию, либо выносить параметры, чтобы их можно было перебирать через pytest.mark.parametrize
не совем понял вашего замечания. Параметризация в pytest работает не так, как вы описали, аргументы добавляются после использования декоратора @pytest.mark.parametrize(...)
Когда дело дойдёт до запуска, эти тесты надо будет либо списком вызывать, либо по файлам распихивать, что одно что другое неудобно. Поэтому существую маркеры.
Из своего опыта скажу, что лучше распихать по логическим папкам. Марки хороши, когда нам необходимо выделить небольшую группу тестов из огромной массы.
Знаю, что мои замечания больше про большой проект, но всё же когда показываешь пример, надо или говорить что это автотест на один эндпоинт или уже показывать как масштабировать. Этот код не подходит для серьёзного проекта ( хотя бы потому что не используется питест)
Повторюсь, что эта статья не про pytest, pytest здесь использован только как ранер
Хоть автор и пишет, что он абстрагируется, толку от выноса импорта особо нет, поменять импорт при переходе на другую библиотеку - самое малое, что нужно будет сделать. Хочешь сделать на асинхронной либе - ну сделай сразу (хотя зачем, если есть xdist - непонятно).
xdist это не решение проблемы. Все зависит от проекта, но при "тысячах" тестах впихнуть xdist та еще задача.
Попробуйте мыслить категориями "у меня здесь 10 000 тестов и как мне с этим жить" )
А что не так с xdist? Я делал запуск xdist на сьюте из 5к API тестов, из которых 2-3к генерировались динамически при старте сьюта (тесты на различные версии апи). Надо учесть пару мелочей загрузки тестов и все.
Что изменилось? Код стал более читаемый
3 раза пришлось возвращаться чтобы понять какой Register за что отвечает
Было очень обнадеживающее вступление, но прочитал и расстроился. Первоначальный тест читаемее в 10 раз итогового варианта.
У вас получился какой-то java подход с какими-то не нужными абстракциями (фабриками)
Зачем выделять в отдельную сущность регистрацию, если в 99% случаев мы напишем на этот кусочек кода 5 тестов и никогда больше к нему не вернемся? (в API тестах, как по мне, page object подход не работает абсолютно, он только создает кучу ненужного кода, раскиданого по всему проекту)
Зачем делать custom_request, но при этом все равно явно передавать ему url? У вас URL сервера не меняется на протяжении тестов, передайте его при инициализации.
Зачем каждой сущности типа Register, создавать своего клиента? http клиент должен быть 1 на весь прогон тестов (также, как и коннекты к базе и т.п.)
Зачем вообще нужен custom_request и свой клиент, если они под собой используют все равно requests? Если хочется свои обработки, то лучше у requests кастомную сессию определить
Зачем делать свою ResponseModel, если она не модель и повторяет ответы от requests? Если назвали моделью, то хоть минимальную валидацию данных в ней реализуйте
Вы упомянули swagger, но никак его не используете, зачем упомянули? По хорошему, на основе схемы надо реализовывать валидацию структуры и типов данных
Первоначальный тест читаемее в 10 раз итогового варианта.
У вас получился какой-то java подход с какими-то не нужными абстракциями (фабриками)
Как я писал выше, попробуйте мыслить не 1 тест, а "у нас тут много тестов и это все надо поддерживать"
Зачем выделять в отдельную сущность регистрацию, если в 99% случаев мы напишем на этот кусочек кода 5 тестов и никогда больше к нему не вернемся? (в API тестах, как по мне, page object подход не работает абсолютно, он только создает кучу ненужного кода, раскиданого по всему проекту)
А как вы предлагаете решить проблему? Писать в лоб? Где та тонкая грань, когда тестов "много" (больше 5?) и стоит писать нужный код и раскидывать по всему проекту
Зачем делать custom_request, но при этом все равно явно передавать ему url? У вас URL сервера не меняется на протяжении тестов, передайте его при инициализации.
Так как в будущем вы будете использовать эти тесты в CI и вам надо будет прокидывать url через командную строку и/или переменную. Поэтому хардкодить не вариант
Зачем каждой сущности типа Register, создавать своего клиента? http клиент должен быть 1 на весь прогон тестов (также, как и коннекты к базе и т.п.)
"Так же хотелось бы отметить, что код ниже про архитектуру, некоторые вещи я намерено упрощаю, что бы не затягивать."
Здесь реализация через фикстуру и "должен быть 1 на весь прогон тестов", как пример
Зачем вообще нужен custom_request и свой клиент, если они под собой используют все равно requests? Если хочется свои обработки, то лучше у requests кастомную сессию определить
инверсия зависимости
Зачем делать свою ResponseModel, если она не модель и повторяет ответы от requests? Если назвали моделью, то хоть минимальную валидацию данных в ней реализуйте
Эта модель не повторяет requests, эта наше собственная модель. В тестах мы независим от нижней реализации - requests. Где делать валидацию это вопрос для дальнейшего обсуждения, к сожалению, мне не хватит бумаги что бы за один заход рассказать как писать api тесты
Вы упомянули swagger, но никак его не используете, зачем упомянули? По хорошему, на основе схемы надо реализовывать валидацию структуры и типов данных
Используем, сваггер это "книга данных", к в которую автотестор постоянно заглядывает и смотрит. В идеальной компании такой подход, когда мы доверимся полностью сваггеру и будем на основе его писать модели, сработал бы. Но я никогда не встречал "идеальный" сваггер, который 100% отражает действительность. Сваггер для меня это внешняя зависимость, а такие зависимости стараются убирать.
Как я писал выше, попробуйте мыслить не 1 тест, а "у нас тут много тестов и это все надо поддерживать"
Я выше писал, поддерживал ~5к апи тестов с различными версиями. Сначала тоже делал абстракции на абстракцию, потом все сильно упрощал, т.к. чем больше кода, тем сложнее стало в нем разбираться и метод "в лоб", сильно упростил написание и сопровождение тестов.
А как вы предлагаете решить проблему? Писать в лоб? Где та тонкая грань, когда тестов "много" (больше 5?) и стоит писать нужный код и раскидывать по всему проекту
Вы сами писали в комментариях, что разделяете тесты по логическим папкам. Зачем для этого тащить регистрацию куда-то дальше модуля проверяющего регистрацию (или всю логическую сущность в виду системы аутентификации)? Определение много/мало это лежит на коллективе, разрабатывающего эти тесты.
Так как в будущем вы будете использовать эти тесты в CI и вам надо будет прокидывать url через командную строку и/или переменную. Поэтому хардкодить не вариант
Предлагалось, как вы ниже и написали, использовать задавание урла в 1 месте. В вашем же примере данной статьи, вы показываете о задании в каждом тесте/инициализации клиента.
инверсия зависимости
Чтобы что? Боитесь, что requests надо будет заменять?
Эта модель не повторяет requests, эта наше собственная модель. В тестах мы независим от нижней реализации - requests. Где делать валидацию это вопрос для дальнейшего обсуждения, к сожалению, мне не хватит бумаги что бы за один заход рассказать как писать api тесты
Видимо не хватило этой статьи, чтобы объяснить для чего это, пока что, как и предыдущий вопрос, видится, что это для будущей замены requests.
Используем, сваггер это "книга данных", к в которую автотестор постоянно заглядывает и смотрит. В идеальной компании такой подход, когда мы доверимся полностью сваггеру и будем на основе его писать модели, сработал бы. Но я никогда не встречал "идеальный" сваггер, который 100% отражает действительность. Сваггер для меня это внешняя зависимость, а такие зависимости стараются убирать.
Но вы ведь его повторяете в своих тестах (в репозитории) описывая свои модели и у вас появляется теперь 2 вещи, где описана структура ответов от API и в случае проблемы, какой из реализаций доверять больше?
Я посмотрел ваш репозиторий, который вы указали, и там часть вопросов отпадает, реализовано (на мой взгляд) лучше.
Я выше писал, поддерживал ~5к апи тестов с различными версиями. Сначала тоже делал абстракции на абстракцию, потом все сильно упрощал, т.к. чем больше кода, тем сложнее стало в нем разбираться и метод "в лоб", сильно упростил написание и сопровождение тестов.
Мне трудно представить, что бы тот же фронт, например, писали в лоб, без использования MVC (не берем в расчет библиотеки) - на выходе будет неподдерживаемая каша. Но у вас в фирме приняли такое решение, это работает, ок)
Предлагалось, как вы ниже и написали, использовать задавание урла в 1 месте. В вашем же примере данной статьи, вы показываете о задании в каждом тесте/инициализации клиента.
Статья не про "куда нам деть url" ) Код в статье можно и нужно улучшать, но я бы начал с архитектуры, а потом думал об url
Чтобы что? Боитесь, что requests надо будет заменять?
Я не хочу показаться грубым или надменным и заранее извиняюсь, но когда я писал эту статью, мне с одной стороны казалось, что я буду писать про очевидные вещи, даже несмотря на то, сколько кода я видел в проде автотестеров. Но нет, я получил хороший, полезный и интересный отклик)
Но вы ведь его повторяете в своих тестах (в репозитории) описывая свои модели и у вас появляется теперь 2 вещи, где описана структура ответов от API и в случае проблемы, какой из реализаций доверять больше?
Например, в сваггере могут не описать 400 ошибки (и их модели) или копипастнуть. Если бы мой код лежал в одной репе с бэком(на питоне), то я просто бы забрал модели и переиспользовал бы их.
Переписываем API тесты