Allure-Framework. Работа с кодом
Продолжая серию публикаций о возможностях Allure-framework, сегодня мы поговорим о работе с кодом. Под катом разбираем, что такое шаг теста, как выводить информацию в отчет при выполнении шагов и какие бывают категории дефектов. Кроме того, расскажем об аннотациях Allure. Дальше еще интереснее!
Шаг теста. Определение и аннотация @Step
Для визуализации процессов, протекающих в ваших тестах, необходимо использовать шаги. Шаг должен представлять какое-то атомарное действие или проверку. Собственно последовательность шагов и представляет ваш тест. Для наглядности в шаге должны быть описаны входные параметры, а также ожидаемый и фактический результат, если шаг представляет собой проверку.
В Allure для обозначения тестового шага над методом необходимо проставить аннотацию @Step. Затем необходимые шаги-методы включаются в тело тестового метода: тесты с аннотацией @Test.
Рассмотрим пример
Создадим метод с аннотацией @Step, который проверяет соответствие суммы двух слагаемых ожидаемому результату:
@Step public static void checkSumStep(int num1, int num2, int expectedSum)
Теперь создадим тестовый метод, использующий данный шаг:
@Test public void simpleTest2()
При прогоне данного автотеста мы получим следующий отчет:

Аннотация @Step умеет принимать параметр «value», который позволяет указывать имя шага на русском языке. Кроме того, имя шага можно параметризировать – передать в него значения аргументов, передающихся в шаг.
Продемонстрируем на примере
@Step("Проверка разности числа и числа ") public static void checkSubtractionStep(int num1, int num2, int expectedResult)
При использовании данного шага в тесте получим отчет:
Стоит отметить, что в случае использования объекта в качестве аргумента метода, есть возможность вывести в названии шага значение одного из полей этого объекта. Для этого необходимо в value указать имя поля в виде
Вывод информации в отчет при выполнении шагов теста. Вложения
При выполнении шагов автотеста бывает очень полезно выводить в отчет разного рода информацию. Для этих целей в Allure служат вложения.
Вложение — это информация различного типа в виде файла, который может быть прикреплен к тестовому шагу или тесту.
Вложения могут иметь 4 характеристики:
• value/name — наименование вложения;
• type — тип информации в соответствии со стандартом MIME;
• content — содержимое вложения;
• fileExtension — опциональное расширение файла вложения, начинающееся с точки.
Вложения к отчету можно прикреплять 2 способами: с помощью аннотации @Attachment и с помощью статического метода addAttachment класса Allure.
Прикрепление вложений с помощью аннотации @Attachment
В данном способе создается метод, возвращающий массив байтов, над ним размещается аннотация @Attachment. При использовании данного метода считанная информация будет добавлена в отчет в виде файла с соответствующим расширением. Наименование вложения будет таким же, как и имя вызываемого метода.
Продемонстрируем на примере
Создадим метод, который будет зачитывать вложение:
@Attachment public static byte[] getBytes(String resourceName) throws IOException < return Files.readAllBytes(Paths.get("src/main/resources", resourceName)); >
Создадим тестовый шаг, который будет использовать данный метод:
@Step("Проверка эквивалентности строки строке ") public static void checkStringEqualsStep(String str1, String str2) throws IOException
Ну и наконец напишем тест, в котором будет использоваться наш шаг:
@Test public void simpleTest4() throws IOException
В результате прогона данного теста получим отчет:

В аннотации @Attachment можно задавать дополнительные уточняющие параметры:
@Attachment(value = "Вложение", type = "application/json", fileExtension = ".txt") public static byte[] getBytesAnnotationWithArgs(String resourceName) throws IOException < return Files.readAllBytes(Paths.get("src/main/resources", resourceName)); >
При использовании данного метода в своих шагах attachment получит наименование «Вложение», содержимое вложения подсветится в соответствии с шаблоном «json», а само вложение будет сохранено в формате «.txt»:

Если поменять тип на «plain/text», то никакой характерной для json-style подсветки ключей-значений уже не будет:
Также можно поэкспериментировать с fileExtension, например, указать «.doc». В этом случае вложение будет сохранено в формате, характерном для MS Word ’97.
Прикрепление вложений с помощью статического метода addAttachment класса Allure
В данном способе можно прикрепить вложение к шагу теста/тесту, используя перегруженный статический метод addAttachment из класса Allure. В этот метод можно передавать до 4 аргументов — 2 из них обязательны (имя вложения и сам прикладываемый контент), 2 опциональны (расширение файла и MIME-тип).
@Step("Добавить ссылку на Сбербанк") public static void addLinkSber() < String link = "http://sberbank.ru"; Allure.addAttachment("Результат", "text/plain", link); >
Несмотря на то, что MIME-типы необходимо указывать довольно часто, их приходится прописывать «вручную». В поставку Allure не входит класс с константами допустимых типов.
Другие аннотации Allure
Кроме наиболее известных аннотаций @Step и @Attachment, Allure в своем составе имеет целый ряд других аннотаций:
Аннотация @Description
@Description — аннотация, размещаемая над тестом или шагом. Позволяет прикрепить описание к тесту или шагу теста. Данная аннотация может принимать параметры «value» — текст описания и «useJavaDoc». При установке значения «true» последнему параметру, в качестве описания будет браться джавадок, размещенный над методом.
Приведем пример использования данной аннотации
@Test @Description(value = "Тест проверяет эквивалентность единицы единице") public void simpleTest7_1()

Аннотации функциональности
@Epic — аннотация, размещаемая над тестом. Позволяет группировать тесты по эпикам. Данная аннотация принимает параметр «value» — наименование эпика.
@Epics — тоже самое, что и @Epic, но в качестве параметра «value» принимает массив Epic’ов.
@Feature — аннотация, размещаемая над тестом. Позволяет группировать тесты по проверяемому функционалу. Данная аннотация принимает параметр «value» — наименование функционала.
@Features — тоже самое, что и @Feature, но в качестве параметра «value» принимает массив Feature’ей.
@Story — аннотация, размещаемая над тестом. Позволяет группировать тесты по User story. Данная аннотация принимает параметр «value» — наименование User story.
@Stories — тоже самое, что и @Story, но в качестве параметра «value» принимает массив Story’й.
Аннотации @Epic/Epics, @Feature/Features, @Story/Stories можно отнести к аннотациям функциональности. Данные аннотации группируют тесты по функциональности в разделе Behaviors(Функциональность) Allure-отчета.
Приведем пример использования данных аннотаций
@Epic(value = "Математика") @Feature(value = "Простые математические операции") @Story(value = "Сложение") @Test public void sumTest() < Steps.checkSummationStep(5, 4, 9); >@Epic(value = "Математика") @Feature(value = "Простые математические операции") @Story(value = "Вычитание") @Test public void subTest() < checkSubtractionStep(8, 2, 6); >@Epics(value = <@Epic(value = "Математика"), @Epic(value = "Геометрия")>) @Features(value = <@Feature(value = "Тригонометрия"), @Feature(value = "Простые математические операции")>) @Stories(value = <@Story(value = "Синус"), @Story(value = "Синусоида")>) @Test public void checkSinTest()
При выполнении тестов и последующем формировании отчета, в разделе Behaviors мы увидим, что тесты сгруппированы в многоуровневый список (@Epic → @Feature → @Story):

Аннотация @Flaky
@Flaky — аннотация, размещаемая над тестом. Позволяет пометить автотест как нестабильный. Если автотест падает хотя бы в одном перезапуске (например, папка «target» между прогонами одного и того же теста не очищается) — в отчете на данном тесте вы увидите знак бомбы. Кроме того, данной аннотацией можно помечать классы с нестабильными тестами.
Приведем пример использования данной аннотации
Обратите внимание, что ветвления в тестах делать не стоит: в данном случае блок if — else используется лишь для того, чтобы сделать тест нестабильным!
@Test @Flaky public void testDemoFlaky() < int randomNum = ThreadLocalRandom.current().nextInt(0, 2); if (randomNum == 0) < Assert.assertTrue(true); >else < Assert.assertTrue(false); >>
При нескольких прогонах теста в режиме перезапуска отчет примет следующий вид:

Аннотации ссылок на тест-кейсы и дефекты
@Issue — аннотация, размещаемая над тестом. Позволяет линковать автотесты с заведенными Issue. Данная аннотация принимает параметр «value», в котором указывается ID дефекта в баг-треккинговой системе.
@Issues — тоже самое, что и @Issue, но в качестве параметра «value» принимает массив Issue.
@TmsLink — аннотация, размещаемая над тестом. Позволяет линковать автотесты с тестовыми кейсами, шаги которых описаны в системах управления тестированием. Данная аннотация принимает параметр «value», в котором указывается ID теста в системе управления тестированием.
@TmsLinks — тоже самое, что и @TmsLink, но в качестве параметра «value» принимает массив TmsLink’ов.
Аннотации данной группы добавляют ссылки на дефект/тест-кейс в Allure-отчет.
Чтобы из ID дефекта/тест-кейса, указанного в параметре «value», получить полноценную ссылку, необходимо в allure.properties (который должен быть размещен в classpath, например, в src/test/resources) описать шаблон ссылки до соответствующего дефекта/тест-кейса.
allure.link.issue.pattern=https://example.org/issue/<> allure.link.tms.pattern=https://example.org/tms/<>
При генерации отчета Allure подставит вместо символов <> значения, которые были указаны в параметре value Вашей аннотации, и вы получите полноценные ссылки.
Приведем пример использования данных аннотаций
@Test @Issue(value = "FGY-4627") @TmsLinks(<@TmsLink(value = "TL-135"), @TmsLink(value = "TL-158")>) public void simpleTest15() < Assert.assertTrue(1 == 1); >@Test @TmsLink(value = "TL-678") public void simpleTest18()

Аннотации прочих ссылок
@Link — аннотация, размещаемая над тестом. Позволяет приложить к автотесту ссылки на какие-то внешние ресурсы. Данная аннотация может принимать параметры «name» — наименование ссылки (по умолчанию, url), «value» — синоним «name», «url» — адрес, по которому нужно выполнить переход и «type» — параметр, применяемый для создания иконки для ссылки.
@Links — тоже самое, что и @Link, но в качестве параметра «value» принимает массив Link’ов.
Приведем пример использования данных аннотаций
@Test @Link(name = "Ссылка", url = "http://yandex.ru") public void checkSubtractionWithLinkTest() < checkSubtractionStep(15, 5, 10); >@Test @Links(value = <@Link(name = "Ссылка1", url = "http://sberbank.ru"), @Link(name = "Ссылка2", url = "http://yandex.ru")>) public void checkSubtractionWithLinksTest()

Аннотация @Owner
@Owner — аннотация, размещаемая над тестом. Позволяет указать ответственное лицо за тест. Данная аннотация принимает параметр «value», в котором указывается информация об авторе автотеста.
Приведем пример использования данной аннотации
@Test @Owner(value = "Пупкин Валерий Иванович") public void testDemoOwner()

Аннотация @Severity
@Severity — аннотация, размещаемая над тестом. Позволяет указать уровень критичности функционала, проверяемого автотестом. Данная аннотация принимает параметр «value», который может быть одним из элементов enum SeverityLevel: BLOCKER, CRITICAL, NORMAL, MINOR или TRIVIAL.
Приведем пример использования данной аннотации
@Test @Severity(value = SeverityLevel.BLOCKER) public void testDemoSeverity()

Параметризованные автотесты в Allure
Allure умеет работать с параметризованными автотестами. Рассмотрим на примере JUnit 4.12.
Для начала создадим тестовый класс с параметризованными тестами следующего вида:
@RunWith(Parameterized.class) public class ParamsTests < @Parameter public int operand1; @Parameter(1) public int operand2; @Parameter(2) public int expectedResult; @Parameters(name = "operand1 = | operand2 = | expectedResult = ") public static Collection dataProvider() < return Arrays.asList(new Integer[][]< , , , , , , >); > @Test public void checkSum() < Assert.assertTrue("Сумма слагаемых не соответствует ожидаемому значению", operand1 + operand2 == expectedResult); >>
Теперь прогоним наш тест и соберем отчет:

Allure представляет наш параметризованный тест как набор тестов, если провалиться в какой-либо тест, то мы получим детальную информацию о прогоне именно этого кейса.
Категории дефектов. Распределение дефектов по категориям
По умолчанию на вкладке «Categories» Allure-отчета все прогнанные тестовые методы делятся на 2 категории: дефекты продукта (failed tests) и дефекты теста (broken tests). Для того, чтобы сделать кастомную классификацию, необходимо подложить файл categories.json в директорию «target/allure-results».
Если в pom.xml у Вас подключен allure-maven плагин, то categories.json можно разместить в поддиректории resources директории test
Приведем пример кастомной классификации дефектов. Создадим файл categories.json:
• name — наименование категории. Оно будет отображаться на вкладке «Categories» на самом верхнем уровне классификации. Обязательный атрибут.
• matchedStatuses — список статусов, с одним из которых должен завершиться тест, чтобы попасть в данную категорию. Из коробки доступны статусы «failed», «broken», «passed», «skipped» и «unknown». Необязательный атрибут.
• messageRegex — регулярное выражение, которому должно соответствовать Exception-сообщение, чтобы попасть в данную категорию. Необязательный атрибут.
• traceRegex — регулярное выражение, которому должно соответствовать Exception-StackTrace, чтобы попасть в данную категорию. Необязательный атрибут.
Теперь прогоним тесты, обнаруживающие такие дефекты, и посмотрим, как будет выглядеть отчет на вкладке «Categories»:

Тестовое окружение. Блок ENVIRONMENT на главной странице отчета
Allure-отчет позволяет выводить на своей главной странице в специальном блоке информацию о тестовом окружении, на котором были прогнаны тесты. Выглядит это так:

Информация, которая отображается в данном блоке, попадает туда из специального файла environment.properties.
Приведем пример данного файла:
OS.version=Windows 10 Pro JDK.version=jdk1.8.0_162 MAVEN.version=Apache Maven 3.5.2 allure-junit4.version=2.6.0 allure-report.version=2.6.0
Данный файл необходимо подложить в директорию «target/allure-results» до сборки html-отчета. Сделать это можно вручную, а можно использовать maven-resources-plugin.
Приведем пример его использования в данной ситуации, при условии размещения environment.properties в поддиректории resources директории test. Для этого модифицируем pom.xml:
. maven-resources-plugin 3.0.2 copy-resources validate copy-resources $ src/test/resources environment.properties .
Теперь при сборке проекта environment.properties будет попадать в «target/allure-results» и участвовать в построении html-отчета.
При запуске тестов на Jenkins, categories.json не будет самостоятельно копироваться в «target/allure-results». Его также очень удобно добавить в секцию includes maven-resources-plugin’а.
Заключение
Во второй части лонгрида мы постарались подробно рассказать об аннотациях в Allure, привели примеры их использования. Затронули категории дефектов, тестовое окружение и способы вывода информации в отчет. Приглашаем к обсуждению в комментариях.
Подписывайтесь на блог ЕФС, следите за новыми публикациями. Скоро мы снова расскажем что-то новое и полезное про Allure.
- allure
- allure first steps
- allure framework
- allure 2.0
- allure 2
- Блог компании Сбер
- Тестирование IT-систем
- Программирование
- Тестирование веб-сервисов
- Учебный процесс в IT
Saved searches
Use saved searches to filter your results more quickly
Cancel Create saved search
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. You switched accounts on another tab or window. Reload to refresh your session.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Test duplication in the report when dynamically applying the same title or story multiple times #1883
acqa opened this issue Dec 15, 2020 · 5 comments
Test duplication in the report when dynamically applying the same title or story multiple times #1883
acqa opened this issue Dec 15, 2020 · 5 comments
type:bug Something isn’t working
Comments
acqa commented Dec 15, 2020 •
I’m submitting a .
- [x ] bug report
What is the current behavior?

При использовании динамических story и title, title-ы не группируются по имени — появляется большое кол-во title-ов с одинаковым названием и временем выполнения.

При использовании статического story — группировка производится ожидаемо. Как видно — дублирующих тестов (title-ов) — нет.
If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem
Можно выполнить следующий код в pytest
import schemathesis import allure schema = schemathesis.from_uri("https://petstore.swagger.io/v2/swagger.json") #@allure.story("Static story") @schema.parametrize() def test_api(case): allure.dynamic.title(case.method) allure.dynamic.story(case.endpoint.path) #allure.dynamic.title(str(case.method) + ": " + str(case.endpoint.path)) with allure.step("Endpoint: " + str(case.formatted_path) + "; Query: " + str(case.query) + "; Body: " + str(case.body) ): resp = case.call() resp_to_attach = resp.request.method \ + "\n" + resp.request.url \ + "\n" + str(resp.status_code) \ + "\n" + resp.text allure.attach(resp_to_attach, name="output", attachment_type=allure.attachment_type.TEXT) case.validate_response(resp)
Для получения результата со статическим story, можно убрать # у закомментированных строк и закомментировать
allure.dynamic.title(case.method) allure.dynamic.story(case.endpoint.path)
What is the expected behavior?
При использовании динамического story, группировка title-ов должна сохраняться.
What is the motivation / use case for changing the behavior?
Хотелось бы добавить поддержку allure в schemathesis
Please tell us about your environment:
- Allure version: 2.13.6
- Test framework: pytest@3.7.2
- Allure adaptor: allure-pytest-2.7.1
Other information
Обновил информацию, добавил скрины с отчетами.
//: # (
. e.g. detailed explanation, stacktraces, related issues, suggestions
. how to fix, links for us to have more context, eg. Stackoverflow, Gitter etc
)
The text was updated successfully, but these errors were encountered:
Stranger6667 commented Dec 15, 2020
I am not sure what is the default communication language here, as the issue author wrote it in Russian. I’ll provide my comment also in Russian.
Хотелось бы добавить поддержку allure в schemathesis
Здесь важно учесть, что тесты которые генерирует Schemathesis это в первую очередь hypothesis тесты. Их особенность в том что тело функции выполняется множество раз внутри одного pytest теста — там разные этапы, включая генерацию данных + минимизацию. Тем самым все вызовы allure.dynamic.title и прочее выполняются множество раз. Больше деталей можно узнать в этом issue, но во многом это больше вопрос интеграции hypothesis с pytest, чем schemathesis с pytest-allure.
Со своей стороны могу предложить добавить хуки в pytest плагин в schemathesis. Т.е. можно сделать два хука (по-крайней мере на первый взгляд), один бы вызывался до вызова schemathesis-теста, а один после. Чтото вроде такого:
def pytest_schemathesis_before_call(endpoint): . def pytest_schemathesis_after_call(exception, case): .
И можно сделать чтобы они вызывались один раз на каждый endpoint — и внутри них уже можно было бы вызывать allure. Дайте знать если такое подойдет — добавлю в roadmap в Schemathesis.
P.S. To the plugin maintainers. Let me know if it makes sense to give the comment above in English as well.
acqa commented Dec 15, 2020 •
Дмитрий привет! Обновил стартовый пост, добавил скрины отчетов. Первый раз оформляю тут баг, сначала не разобрался как правильно их вставлять.
Тем самым все вызовы allure.dynamic.title и прочее выполняются множество раз.
Как видно из скринов, если использовать антотацию @allure.story(«Static story») над тестовой функцией, то проблем с множественным выполнением allure.dynamic.title не возникает — отчет формируется без дубликатов тестов (title-ов).
А если использовать allure.dynamic.story() вместе с allure.dynamic.title в самой тестовой функции, то получаем дубликаты строк title-ов в отчете. Поэтому предположил, что ошибка на стороне формирователя результата allure.
Если я правильно понял, то там речь про глубокую интеграцию hypothesis c pytest- ом. В моем примере, assert-ов для pytest-а нет совсем, я полагаюсь только на ошибки, которые выведет сам hypothesis.
Для того, чтобы было проще эти ошибки разбирать, я каждый из запросов обрамляю allure.step-ом, в котором прописываю условия выполнения каждого запроса и прикладываю условия и результат http-запроса. В принципе, allure.dynamic.title со статическим @allure.story() позволяет определенную группировку производить. Но хочется сделать группировку чуть детальнее:
Endpoint
— GET
— POST
— PUT
— DELETE
А для этого, нужно как-то в @allure.story() передать текущее значение из case.endpoint.path. Иного способа, кроме как allure.dynamic.story(), пока не придумал — но это решение вызывает некорректное оформление теста.
Со своей стороны могу предложить добавить хуки в pytest плагин в schemathesis.
Если это позволит передавать в @allure.story() текущие параметры из фикстуры case (в частности, case.endpoint.path), то было бы хорошо 🙂
Тесты на pytest с генерацией отчетов в Allure с использованием Docker и Gitlab Pages и частично selenium
Этот текст предназначен для начинающих тестировщиков, желающих понять как делать отчеты на allure с историей тестов, также разъяснить где их хранить, чтобы в отчет мог заглянуть любой участник вашей команды.

Когда я хотел добавить в gitlab автотесты в стеке python, allure, docker, то я выяснил, что толковых статей на эту тему нет. Пришлось разбираться самостоятельно и как результат проб и ошибок появилась эта статья, которая скорее является гайдом, частично затрагивающим написание тестов, но наибольший фокус именно на выстраивании инфраструктуры. Если у вас уже написаны тесты на allure, то вы сразу можете переходить к разделу настройки инфраструктуры. Отмечу, что текст НЕ затрагивает написание UI тестов, но я затрону инфраструктуру для них в отдельном блоке.
Здесь не описаны лучшие практики по написанию тестов и выстраиванию инфраструктуры. Скорее ознакомление с тем, как можно все это организовать. Примеры выполнены на windows, на других системах подход примерно такой же.
Этот гайд предполагает, что у вас уже установлен python, настроен пустой репозиторий, куда вы будете отправлять свой код. При чтении статьи не обязательно знать pytest, но очень желательно знать что из себя представляет фикстура, как ей пользоваться, а так же параметризация тестов. Для тестирования будет использован Dog API. Если вас интересует локальный запуск тестов через gitlab, то вам необходимо установить docker.
Если у вас не установлено вышеперечисленное ПО, то ниже вы можете найти полезные ссылки:
- Установка Python
- Установка Allure Если вы хотите генерировать отчеты локально. В данной статье такой метод не рассматривается.
- Скачать PyCharm Community
- Настройка ssh в git для отправки кода в gitlab
- Pytest — краткое руководство
Подготовка к написанию тестов
Я назвал свой репозиторий для этой статьи allure_pages и скопировал его на свой компьютер.
Нужно создать такую структуру в директории с репозиторием:

- conftest.py — будет использован для написания фикстур
- requirements.txt — для установки необходимых модулей в контейнере с python
- test_dog_api.py внутри отдельной директории tests для написания самих тестов
Далее создаем виртуальное окружение, которое будет использоваться для локального запуска тестов. В папке с проектом создаем его через командную строку с помощью следующей команды:
python -m venv venv
Открываем папку с репозиторием как проект в PyCharm

Удобнее всего, на мой взгляд устанавливать нужные нам пакеты через встроенный в PyCharm терминал. Для этого нужно добавить интерпретатор, и открыть терминал.
- Жмем на Add Interpreter и если вы ранее создали в директории с проектом виртуальное окружение, то PyCharm сразу найдет нужный интерпретатор, который будет использован в проекте
- Открываем терминал по этой кнопке

Устанавливаем нужные пакеты для наших тестов в виртуальном окружении:
Для этого введите в терминал следующие команды.
- pip install requests — для запросов, которые мы будем делать к сервису Dog Api
- pip install pytest — для тестов, само собой
- pip install pytest-xdist — для параллелизации тестов
- pip install allure-pytest — для генерации отчетов в allure
Очень важно после этого написать в терминале
pip freeze > requirements.txt .
Файл requirements.txt нам понадобится для запуска тестов внутри докер контейнера.
Если у вас возникла проблема с установкой пакетов, то можете воспользоваться такими зависимостями для requirements.txt
allure-pytest==2.8.18 pytest==6.0.1 pytest-xdist==2.1.0 requests==2.24.0
Написание фикстуры и небольшого клиента для API тестов
Для того, чтобы упростить наши тесты создадим небольшой клиент для GET и POST запросов. Далее будет подробное описание этого клиента. Если вам это не нужно или кажется сложным, то пропустите эту часть статьи и переходите к следующей. На примере её использования станет ясно как пользоваться клиентом.
Сама фикстура
@pytest.fixture def dog_api(): return ApiClient(base_address="https://dog.ceo/api/")
Ниже конструктор класса для клиента. При инициализации объекта класс принимает в себя аргумент base_address, на его место нужно записать в фикстуре общий адрес для всех запросов в нашем случае будет использован https://dog.ceo/api/ . Обратите внимание, что фикстура возвращает объект с базовым адресом для дальнейших GET и POST запросов.
class ApiClient: def __init__(self, base_address): self.base_address = base_address
Добавим в класс метод GET для отправки запросов.
def get(self, path="/", params=None, headers=None): url = f"" return requests.get(url=url, params=params, headers=headers)
Здесь можно заметить, что наш клиент является оберткой над библиотекой requests. В классе ApiClient аргументы path , params , headers метода get выступают в роли аргументов, которые передаются в requests.get(url=url, params=params, headers=headers) . Именно эта строчка и отвечает за выполнение запросов. Из адреса, использованного при инициализации объекта и пути, который мы передадим в дальнейшем в ходе теста, будет складываться полный адрес, который и будет использоваться в requests.get .
Добавим в класс метод POST. Его логика идентична, просто добавляется несколько аргументов, которые при желании можно заполнить данными.
def post(self, path="/", params=None, data=None, json=None, headers=None): url = f"" return requests.post(url=url, params=params, data=data, json=json, headers=headers)
В итоге у нас получается такой файл conftest.py:
import pytest import requests class ApiClient: def __init__(self, base_address): self.base_address = base_address def post(self, path="/", params=None, data=None, json=None, headers=None): url = f"" return requests.post(url=url, params=params, data=data, json=json, headers=headers) def get(self, path="/", params=None, headers=None): url = f"" return requests.get(url=url, params=params, headers=headers) @pytest.fixture def dog_api(): return ApiClient(base_address="https://dog.ceo/api/")
Пишем тесты
В самом сервисе Dog API есть много разных запросов. Если у вас есть опыт в написании тестов, то можете самостоятельно написать несколько тестов. Так как акцент в этой статье заключается в постройке инфраструктуры для тестов, я не буду вдаваться в подробности тестов. Ранее я это сделал, чтобы мой код был более менее понятен. Несколько параметризованных тестов специально сломаны, чтобы показать как они будут показаны в отчете. Так выглядит мой файл с тестами test_dog_api.py
import pytest def test_get_random_dog(dog_api): response = dog_api.get("breeds/image/random") with allure.step("Запрос отправлен, посмотрим код ответа"): assert response.status_code == 200, f"Неверный код ответа, получен " with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."): response = response.json() assert response["status"] == "success" with allure.step(f"Посмотрим что получили "): with allure.step(f"Вложим шаги друг в друга по приколу"): with allure.step(f"Наверняка получится что-то интересное"): pass @pytest.mark.parametrize("breed", [ "afghan", "basset", "blood", "english", "ibizan", "plott", "walker" ]) def test_get_random_breed_image(dog_api, breed): response = dog_api.get(f"breed/hound//images/random") response = response.json() assert breed in response["message"], f"Нет ссылки на изображение с указанной породой, ответ " @pytest.mark.parametrize("file", ['.md', '.MD', '.exe', '.txt']) def test_get_breed_images(dog_api, file): response = dog_api.get("breed/hound/images") response = response.json() result = '\n'.join(response["message"]) assert file not in result, f"В сообщении есть файл с расширением " @pytest.mark.parametrize("breed", [ "african", "boxer", "entlebucher", "elkhound", "shiba", "whippet", "spaniel", "dvornyaga" ]) def test_get_random_breed_images(dog_api, breed): response = dog_api.get(f"breed//images/") response = response.json() assert response["status"] == "success", f"Не удалось получить список изображений породы " @pytest.mark.parametrize("number_of_images", [i for i in range(1, 10)]) def test_get_few_sub_breed_random_images(dog_api, number_of_images): response = dog_api.get(f"breed/hound/afghan/images/random/") response = response.json() final_len = len(response["message"]) assert final_len == number_of_images, f"Количество фото не , а "
Добавляем allure в тесты
Есть много разных подходов к написанию тестов с помощью allure. Так как сейчас рассматривается больше инфраструктура, то добавим совсем простые шаги. Для понимания написанного мною кода, я все же прокомментирую несколько моментов.
with allure.step(‘step 1’): — с помощью этого контекстного менеджера тело теста делится на шаги, понятные в отчете.
@allure.feature(‘Dog Api’) @allure.story(‘Send few requests’) — декораторы, с помощью которых сами тесты или тестовые наборы будут структурированы в отчете
Это лишь малая часть возможностей allure, но для выстраивания инфраструктуры и дальнейшей доработки тестов этого хватит. Если вы хотите во всей полноте пользоваться возможностями allure, то рекомендую ознакомиться с документацией
Добавим в наш API клиент несколько allure шагов.
class ApiClient: def __init__(self, base_address): self.base_address = base_address def post(self, path="/", params=None, data=None, json=None, headers=None): url = f"" with allure.step(f'POST request to: '): return requests.post(url=url, params=params, data=data, json=json, headers=headers) def get(self, path="/", params=None, headers=None): url = f"" with allure.step(f'GET request to: '): return requests.get(url=url, params=params, headers=headers)
Добавим декораторы в тесты и снабдим их шагами. Так же добавлю излишние шаги, чтобы показать зависимость генерации отчета от кода. Здесь не описаны лучшие практики по написанию текста для шагов. Текст намеренно неформальный для простоты восприятия кода и отчета.
@allure.feature('Random dog') @allure.story('Получение фото случайной собаки и вложенные друг в друга шаги') def test_get_random_dog(dog_api): response = dog_api.get("breeds/image/random") with allure.step("Запрос отправлен, посмотрим код ответа"): assert response.status_code == 200, f"Неверный код ответа, получен " with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."): response = response.json() assert response["status"] == "success" with allure.step(f"Посмотрим что получили "): with allure.step(f"Вложим шаги друг в друга по приколу"): with allure.step(f"Наверняка получится что-то интересное"): pass
Сходным образом заполним шагами и декораторами остальные тесты. Финальный файл test_dog_api.py
import pytest import allure @allure.feature('Random dog') @allure.story('Получение фото случайной собаки и вложенные друг в друга шаги') def test_get_random_dog(dog_api): response = dog_api.get("breeds/image/random") with allure.step("Запрос отправлен, посмотрим код ответа"): assert response.status_code == 200, f"Неверный код ответа, получен " with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."): response = response.json() assert response["status"] == "success" with allure.step(f"Посмотрим что получили "): with allure.step(f"Вложим шаги друг в друга по приколу"): with allure.step(f"Наверняка получится что-то интересное"): pass @allure.feature('Random dog') @allure.story('Фото случайной собаки из определенной породы') @pytest.mark.parametrize("breed", [ "afghan", "basset", "blood", "english", "ibizan", "plott", "walker" ]) def test_get_random_breed_image(dog_api, breed): response = dog_api.get(f"breed/hound//images/random") with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."): response = response.json() assert breed in response["message"], f"Нет ссылки на фото с указанной породой, ответ " @allure.feature('List of dog images') @allure.story('Список всех фото собак списком содержит только изображения') @pytest.mark.parametrize("file", ['.md', '.MD', '.exe', '.txt']) def test_get_breed_images(dog_api, file): response = dog_api.get("breed/hound/images") with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."): response = response.json() with allure.step("Соединим все ссылки в ответе из списка в строку"): result = '\n'.join(response["message"]) assert file not in result, f"В сообщении есть файл с расширением " @allure.feature('List of dog images') @allure.story('Список фото определенных пород') @pytest.mark.parametrize("breed", [ "african", "boxer", "entlebucher", "elkhound", "shiba", "whippet", "spaniel", "dvornyaga" ]) def test_get_random_breed_images(dog_api, breed): response = dog_api.get(f"breed//images/") with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."): response = response.json() assert response["status"] == "success", f"Не удалось получить список изображений породы " @allure.feature('List of dog images') @allure.story('Список определенного количества случайных фото') @pytest.mark.parametrize("number_of_images", [i for i in range(1, 10)]) def test_get_few_sub_breed_random_images(dog_api, number_of_images): response = dog_api.get(f"breed/hound/afghan/images/random/") with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."): response = response.json() with allure.step("Посмотрим длину списка со ссылками на фото"): final_len = len(response["message"]) assert final_len == number_of_images, f"Количество фото не , а "
Делаем инфраструктуру для тестов
Здесь есть несколько моментов, о которых стоит написать:
- .gitlab-ci.yml — файл, где на языке разметки yaml описаны инструкции что нужно делать gitlab runner
- gitlab-runner — это проект, написанный на языке Go. Он будет выполнять инструкции. Есть несколько вариантов его использования. Мы будем писать инструкцию для gitlab runner, где он в свою очередь будет использовать docker для запуска тестов и всего остального. Далее по тексту он будет обозначаться просто «раннер». Что это за сущность и с чем её едят можно найти здесь.
Если у вас в компании уже настроена инфраструктура и вы можете попросить своего devops выделить вам раннер где-то в облаке, то сделайте это. Раннер можно использовать и локально на своем компьютере. В этом случае вам нужен установленный docker desktop на windows. В этом гайде мы будем использовать локальный раннер, но для облачного запуска вам нужно будет просто указать в .gitlab-ci нужный раннер.
Настройка репозитория
Сам репозиторий должен быть публичным, если вы используете gitlab.com. Если вы используете именно gitlab.com, то настраивать по тексту ниже не нужно. По умолчанию репозиторий уже будет готов ко всему, что нам понадобится для инфраструктуры.
Нужно зайти в настройки репозитория Settings -> General -> Visibility, project features, permissions , активировать Pipelines и сохранить изменения.

После этого в разделе настроек появится раздел CI / CD . Переходим Settings -> CI / CD -> Runners

Здесь переходим по ссылке в пункте 1 и согласно инструкции ставим себе на компьютер Gitlab Runner. Далее в статье адаптация инструкции на русском.
Установка и настройка Gitlab Runner
Ниже инструкция, которую вы можете найти здесь. Можете пройтись по ней самостоятельно. Далее в тексте будет сделано все по инструкции.
- Создайте папку где-нибудь в системе, например: C:\GitLab-Runner.
- Скачайте бинарник для x86 или amd64 и положите его в созданную папку. Переименуйте скачанный файл в gitlab-runner.exe. Вы можете скачать бинарник для любой доступной версии здесь
- Запустите командную строку с правами администратора. (Сам я буду использовать обычный powershell с правами администратора)
- Зарегестрируйте раннер
- Установите раннер как сервис и запустите его. Вы можете запустить службу, используя встроенную системную учетную запись (рекомендуется) или учетную запись пользователя.
С первыми тремя пунктами инструкции все понятно. Создаем папку, кладем в неё переименованный раннер и запускаем в директории powershell, то с четвертым и пятым пунктом могут возникнуть проблемы и недопонимание, если не потратить некоторое время на чтение документации. Далее простыми словами описаны шаги, которые нужно сделать, чтобы раннер заработал.
Я создал папку C:\gitlab_runners и сохранил туда скачанный для моей системы раннер, предварительно переименовав в gitlab-runner.exe
Как только вы справились с первыми тремя пунктами, то нужно сделать следующее:
- Ввести команду:
./gitlab-runner.exe register - Ввести url, указанный на странице настроек раннеров в репозитори в пункте 2. Например, у меня так:
https://gitlab.somesubdomain.com/ - Ввести токен указанный на странице настроек раннеров в репозитори в пункте 3. Например, у меня так:
tJTUaJ7JxfL4yafEyF3k - Вводим описание раннера. Его потом можно будет изменить через UI в настроках раннера в репозитории. Например так:
Runner on windows for autotests - Добавляем теги для раннера, для того, чтобы описывать нужный раннер в .gitlab-ci.yml, используя определенные теги. Это в основном нужно, когда раннеров больше одного.
docker, windows - Выбираем тип раннера, который нам нужен. Здесь выберем docker
docker - Вводим дефолтный image, который будет использоваться раннером. Его можно будет изменить в конфиге раннера или указать конкретый в .gitlab-ci.yml
python:3.8-alpine
Скрншот с выполнеными шагами

Наш раннер зарегистрирован, теперь его нужно запустить.
Проверить статус раннера можно так:
.\gitlab-runner.exe status
Запустите раннер с помощью:
.\gitlab-runner.exe run
Теперь, если вы зайдете в настроки раннеров в репозитории Settings -> CI / CD -> Runners , то увидите что-то такое:

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

Все хорошо, теперь можно перейти к пайплайну.
Настройка пайплайна в .gitlab-ci.yml
Инструкции для раннера описываются в .gitlab-ci.yml . Полная документация описана здесь.
Первым делом нам нужно описать stages. Шаги пайплайна. Их будет 4. Каждый stage — отдельный job, который будет выполнять раннер.
stages: - testing # Запуск тестов - history_copy # Копирование результата тестов из предыдущего запуска тестов - reports # Генерация отчета - deploy # Публикация отчета на gitlab pages
Шаг первый. Testing
docker_job: # Название job stage: testing # Первый stage, который нужно выполнить tags: - docker # С помощью этого тега gitlab поймет, какой раннер нужно запустить. Он запустит докер контейнер, из образа, который мы указывали в 6 шаге регистрации раннера. image: python:3.8-alpine # Нужно указать образ, который будет использоваться для запуска тестов. before_script: - pip install -r requirements.txt # Устанавливаем пакеты в поднятом контейнере перед запуском самих тестов script: - pytest -n=4 --alluredir=./allure-results tests/test_dog_api.py # Запускаем тесты параллельно(-n=4 обеспечивает нам это), указав папку с результатами тестов через --alluredir= allow_failure: true # Это позволит нам продолжить выполнение пайплайна в случае, если тесты упали. artifacts: # Сущность, с помощью которой, мы сохраним результат тестирования. when: always # Сохранять всегда paths: - ./allure-results # Здесь будет сохранен отчет expire_in: 1 day # Да, он будет удален через день. Нет смысла хранить его в течение длительного срока.
Шаг второй. history_copy
history_job: # Название job stage: history_copy # Это второй stage, который нужно выполнить tags: - docker # Пользуемся тем же самым раннером image: storytel/alpine-bash-curl # Но теперь укажем раннеру использовать другой образ, для того чтобы скачать результаты теста из предыдущего пайплайна. Нам же нужна история тестов, верно? script: - 'curl --location --output artifacts.zip "https://(ВАШ ДОМЕН, например gitlab.example.com)/api/v4/projects/(АЙДИ ВАШЕГО РЕПОЗИТОРИЯ)/jobs/artifacts/master/download?job=pages&job_token=$CI_JOB_TOKEN"' # С помощью api гитлаба скачиваем файлы из job, который будет указан ниже. Обратите внимание на текст на русском в ссылке. Очень важно указать вместо текста и скобок номер вашего репозиториия - apk add unzip # Образ, используемый в этом шаге по умолчанию не имеет утилиты unzip, по этой причине мы добавляем её в контейнер - unzip artifacts.zip # Распаковываем файлы - chmod -R 777 public # Даем права любые манипуляции с содержимым - cp -r ./public/history ./allure-results # Копируем историю в папку с результатами теста allow_failure: true # Так как при первом запуске пайплайна истории нет, это позволит нам избежать падения пайплайна. В дальнейшем эту строчку можно спокойно удалить. artifacts: paths: - ./allure-results # Сохраняем данные expire_in: 1 day rules: - when: always # Сохранять всегда
Шаг третий. reports
allure_job: # Название job stage: reports # Третий stage, который будет выполнен tags: - docker # Пользуемся тем же самым раннером image: frankescobar/allure-docker-service # Указываем раннеру использовать образ с allure. В нем мы будем генерировать отчет. script: - allure generate -c ./allure-results -o ./allure-report # Генерируем отчет из ./allure-results внутрь папки ./allure-report artifacts: paths: - ./allure-results # Примонтируем две этих директории для получения результатов тестирования и генерации отчетов соответственно - ./allure-report expire_in: 1 day rules: - when: always
Шаг четвертый. deploy
pages: # Названием этой job говорим гитлабу, чтобы захостил результат у себя в pages stage: deploy # Четвертый stage, который будет выполнен script: - mkdir public # Создаем папку public. По умолчанию гитлаб хостит в gitlab pages только из папки public - mv ./allure-report/* public # Перемещаем в папку public сгенерированный отчет. artifacts: paths: - public rules: - when: always
stages: - testing # Запуск тестов - history_copy # Копирование результата тестов из предыдущего запуска тестов - reports # Генерация отчета - deploy # Публикация отчета на gitlab pages docker_job: # Название job stage: testing # Первый stage, который нужно выполнить tags: - docker # С помощью этого тега gitlab поймет, какой раннер нужно запустить. Он запустит докер контейнер, из образа, который мы указывали в 6 шаге регистрации раннера. image: python:3.8-alpine # Нужно указать образ, который будет использоваться для запуска тестов. before_script: - pip install -r requirements.txt # Устанавливаем пакеты в поднятом контейнере перед запуском самих тестов script: - pytest -n=4 --alluredir=./allure-results tests/test_dog_api.py # Запускаем тесты параллельно(-n=4 обеспечивает нам это), указав папку с результатами тестов через --alluredir= allow_failure: true # Это позволит нам продолжить выполнение пайплайна в случае, если тесты упали. artifacts: # Сущность, с помощью которой, мы сохраним результат тестирования. when: always # Сохранять всегда paths: - ./allure-results # Здесь будет сохранен отчет expire_in: 1 day # Да, он будет удален через день. Нет смысла хранить его в течение длительного срока. history_job: # Название job stage: history_copy # Это второй stage, который нужно выполнить tags: - docker # Пользуемся тем же самым раннером image: storytel/alpine-bash-curl # Но теперь укажем раннеру использовать другой образ, для того чтобы скачать результаты теста из предыдущего пайплайна. Нам же нужна история тестов, верно? script: - 'curl --location --output artifacts.zip "https://(ВАШ ДОМЕН, например gitlab.example.com)/api/v4/projects/(АЙДИ ВАШЕГО РЕПОЗИТОРИЯ)/jobs/artifacts/master/download?job=pages&job_token=$CI_JOB_TOKEN"' # С помощью api гитлаба скачиваем файлы из job, который будет указан ниже. Обратите внимание на текст на русском в ссылке. Очень важно указать вместо текста и скобок номер вашего репозиториия - apk add unzip # Образ, используемый в этом шаге по умолчанию не имеет утилиты unzip, по этой причине мы добавляем её в контейнер - unzip artifacts.zip # Распаковываем файлы - chmod -R 777 public # Даем права любые манипуляции с содержимым - cp -r ./public/history ./allure-results # Копируем историю в папку с результатами теста allow_failure: true # Так как при первом запуске пайплайна истории нет, это позволит нам избежать падения пайплайна. В дальнейшем эту строчку можно спокойно удалить. artifacts: paths: - ./allure-results # Сохраняем данные expire_in: 1 day rules: - when: always # Сохранять всегда allure_job: # Название job stage: reports # Третий stage, который будет выполнен tags: - docker # Пользуемся тем же самым раннером image: frankescobar/allure-docker-service # Указываем раннеру использовать образ с allure. В нем мы будем генерировать отчет. script: - allure generate -c ./allure-results -o ./allure-report # Генерируем отчет из ./allure-results внутрь папки ./allure-report artifacts: paths: - ./allure-results # Примонтируем две этих директории для получения результатов тестирования и генерации отчетов соответственно - ./allure-report expire_in: 1 day rules: - when: always pages: # Названием этой job говорим гитлабу, чтобы захостил результат у себя в pages stage: deploy # Четвертый stage, который будет выполнен script: - mkdir public # Создаем папку public. По умолчанию гитлаб хостит в gitlab pages только из папки public - mv ./allure-report/* public # Перемещаем в папку public сгенерированный отчет. artifacts: paths: - public rules: - when: always
Запуск тестов и просмотр отчета
Осталось отправить в репозиторий все необходимые файлы, а именно:
- conftest.py
- Директорию tests
- requirements.txt (Убедитесь, что там есть все нужные зависимости)
- .gitlab-ci.yml
Отправляем и смотрим что получилось.
В первый раз пайплайн будет запускаться немного дольше, чем в следующие разы. Потому что ему нужно будет скачать нужные образы, запустить их и все такое. Заходим в репозиторий и через сайдбар переходим в CI / CD -> Pipelines
Здесь вы сможете увидеть статус пайплайна, а также перейти к Ci линтеру, где можете проверить конкретно свой .gitlab-ci.yml.

Когда пайплайн пройдет, увидеть ссылку и сам отчет можно через Settings -> Pages . При первом использовании pages в репозитории для просмотра страницы может понадобиться до 30 минут. Проходим по ссылке и смотрим отчет.

Получаем результат. Советую побродить по отчету и посмотреть результаты.

Сломаем несколько тестов, чтобы посмотреть, как будет отображаться история тестов. Затем подождем окончания пайплайна.

Как видите второй шаг в этот раз выполнился без ошибок. Это потому, на stage history_job проблем с копированием истории предыдущих пайплайнов не возникло. Теперь посмотрим результат тестов.

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

Настройка пайплайна для UI тестов
Реализовать их можно с помощью специальной сущности гитлаба [services](https://docs.gitlab.com/ee/ci/services/) . Это ключевое слово для использования докер образов, которые будут запущены до хода шагов в script . Для UI тестов нужно добавить в job пайплайна следующее:
services: - selenium/standalone-chrome:latest
Так как нам нужно обращаться к контейнеру, который поднимет гитлаб по определенному url, то он каким-то образом назначает этому сервису url, по которому можно слать запросы. Логика здесь такая:
- Все после : отбрасывается
- Слеш / заменяется двойным подчеркиванием __ и создается главный алиас
- Слеш / заменяется одиночным дефисом — и создается дополнительный алиас (Необходим Gitlab Runner v1.1.0 и выше)
И теперь нужно изменить в фикстуре executor (по умолчанию используется chromedriver на вашей машине, но сейчас мы все это запускаем в контейнерах) для запуска ui тестов:
browser = webdriver.Remote(command_executor="http://selenium__standalone-chrome:4444/wd/hub")
Обратите внимание, что было в .gitlab-ci.yml:
selenium/standalone-chrome:latest
И по какому адресу нужно обращаться фикстуре для запуска тестов:
selenium__standalone-chrome
Для запуска моих тестов у меня получился такой job. Если добавить к нему три следующих job из примера с api тестами, то можно прогнать все тесты и получить отчет.
chrome_job: stage: testing services: - selenium/standalone-chrome image: python:3.8 tags: - docker before_script: - pip install -r requirements.txt script: - pytest --alluredir=./allure-results tests/ allow_failure: true artifacts: when: always paths: - ./allure-results expire_in: 1 day
Полезные ссылки
Помимо ссылок в статье, хочется поделиться еще несколькими, которые могут помочь в работе с allure и gitlab ci.
UPD: Спасибо комментаторам, за то, что указали мне на неточности и неполноту этого гайда. Я поправил и добавил ссылку на рабочий репозиторий
Почему в allure отчете отсутствует тело теста
Мне понравился механизм аспектно-ориентированного программирования (АОП), который используется в Allure Framework для перехвата выполнения тестовых шагов, отмеченных аннотацией @Step. И я попробовал применить его в автотестировании, не подключая к тестам таких монстров, как Spring или Guice.
В этой статье вы найдете несколько полезных примеров использования аспектов.

В двух словах аспектно-ориентированное программирование — это концепция, разделения функциональности программы для упрощения ее разложения на модули. Здесь я не буду останавливаться на теории, чтобы быстрее перейти к практическим примерам. Неплохая статья для знакомства с подходом есть на Хабре. А в Википедии есть словарь основных терминов АОП.
С точки зрения автотестирования это механизм, который позволяет в существующий большой проект дописать файлик Aspect с так называемыми Advice, которые будут выполняться в нужных местах и добавлять некоторые полезные инструменты в весь проект.
В общем случае выглядит это так
Все тестовые шаги мы помечаем аннотацией @Step — это способ отделить их от остальных методов. Если в ходе работы приложения выполняется метод, который соответствует какому-либо указателю @Pointcut из наших аспектов, то в зависимости от вида Advice-а будет:
- выполнен код Advice, а затем уже метод (@Before);
- выполнен метод, а затем уже Advice (@After, @AfterReturning);
- выполнен код Advice, внутри которого можно запустить исходный метод (один или несколько раз) или же не делать ничего (@Around).
Например, если при выполнении метода вываливается исключение, его можно перехватить и “проглотить” с помощью аспекта, записав при этом в лог. Со стороны же выполнение будет выглядеть так, будто программа выполнилась без ошибок.
Advice имеет доступ к телу метода — мы можем получить параметры, с которыми его вызывают и использовать эти данные для выполнения каких-то своих действий — той же записи в log.
Allure Framework, который мы (и многие другие) используем для формирования отчетов автотестов, построен как раз на механизмах AOP. Allure-адаптер делает ровно то же самое — отлавливает аннотации @Step в последовательности выполняющихся методов. А описание того, что необходимо сделать при обнаружении аннотации, хранится в аспекте. Среди прочего там есть добавление информации о шаге в отчет. Если шаг выполняется успешно, он помечается в отчете зеленым, если нет — красным.
Честно говоря, я и набрел на аспектно-ориентированное программирование, пока ковырялся в Allure, пытаясь реализовать там необходимое мне поведение. Наверное, для моих целей можно было бы использовать тот же Lombok, но библиотека AspectJ уже есть в комплекте с Allure и легко внедряется в наши проекты. Плюс на примере Allure было отлично видно, как с ее помощью получить то, что мне нужно. А усложнение и увеличение числа используемых инструментов не входило в планы.
Полезные примеры применения аспектов в автотестах
Для сборки я использую Gradle, в Maven все будет немного иначе.
Прежде чем переходить к конкретным шагам, необходимо подключить AspectJ с помощью зависимостей:
compile 'org.aspectj:aspectjrt:1.9.2' compile ‘org.aspectj:aspectjweaver:1.9.2’
Я использую плагин io.qameta.allure:allure-gradle:2.8.1, в котором уже есть библиотека “aspectjweaver”, т.е. для Allure достаточно указать версию:
allure
Исходники примеров, изложенных ниже, вы можете найти в моем GitHub.
Сквозное логирование
Аспекты позволяют добавить сквозное логирование ко всему проекту. Для этого достаточно создать всего лишь один класс, который будет записывать в log-файл данные о выполнении метода, обернутого в аспект.
Предположим, у нас есть простейший тест, который открывает главную страницу Google, выполняет поиск по какому-то запросу и сравнивает результаты с ожидаемыми (берет один из веб-элементов на странице — в нашем случае строку с количеством результатов — и проверяет совпадение с заданным текстом).
@Test public void allureLogTest ()
В этом примере я специально задал кривой текст для сравнения, чтобы тест упал.
Если мы не используем другие инструменты (например, встроенный механизм Selenide, который позволяет добавлять своих слушателей на разные события), то тест будет работать молча, не сообщая о текущих выполняемых шагах в консоль.
Под капотом у нас есть аннотации @Step, которые мы используем для шагов, попадающих в отчет Allure. Но по умолчанию эти шаги в консоль не выводятся.
public class GoogleSteps < @Step("Получаем сводку по результатам поиска") @Attachment public static String getResultStats() < return Selenide.$("#result-stats").text(); >@Step("Выполняем поиск по запросу: ") public static void searchFor(String request) < Selenide.$("[name='q']").setValue(request); Selenide.$("[name='btnK']").click(); >@Step("Открываем страницу поиска") public static void openSearchPage() < Selenide.open("https://www.google.ru/"); >>
Вывести их в консоль с минимальными вмешательствами можно с помощью аспектов. Достаточно создать аспект — фактически, обычный класс java со специальной аннотацией — который будет выделять название шагов @Step.
@Aspect public class AllureLogAspect
Внутри мы должны указать pointcut, где будет срабатывать аспект. Допустим, нам нужно, чтобы он срабатывал перед запуском любого метода, в котором проставлена аннотация io.qameta.allure.Step:
@Before("@annotation(io.qameta.allure.Step) && execution(* *(..))") public void beforeStep(JoinPoint joinPoint)
@Before означает, что аспект должен сработать перед запуском метода. В этот метод передается точка присоединения (JoinPoint), содержащая указатель на запускаемый метод. Здесь же мы можем получить параметры запуска:
private String getStepName(JoinPoint joinPoint) < MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); MapparametersMap = getParametersMap(joinPoint); Method method = methodSignature.getMethod(); Step step = method.getAnnotation(Step.class); String stepName = step.value(); return Optional.of(stepName) .filter(StringUtils::isNoneEmpty) .map(it -> processNameTemplate(it, parametersMap)) .orElse(methodSignature.getName()); >
Отмечу, что вместо
return Optional.of(stepName) .filter(StringUtils::isNoneEmpty) .map(it -> processNameTemplate(it, parametersMap)) .orElse(methodSignature.getName());
можно было написать просто
return stepName;
Но в этом случае мы получили бы просто исходный текст аннотации и не имели бы возможности заменить подстановочные символы. Чуть более сложная запись позволяет при открывании страницы увидеть в консоли ее адрес, переданный в метод.
Что мы получаем в результате?
При запуске теста еще до открытия страницы выполняется метод beforeStep, из него мы выделяем адрес страницы и пишем в лог. Дальше управление возвращается исходному методу.
Чтобы лог был полным после его выполнения мы подставим еще один аспект с @AfterReturning:
@AfterReturning( pointcut = "@annotation(io.qameta.allure.Step) && execution(* *(..))", returning = "result" ) public void afterStep(JoinPoint joinPoint, Object result) < String stepName = getStepName(joinPoint); log.info("END: " + stepName); if(result != null) < log.info("RESULT: " + result); >>
Здесь помимо аналогичной записи в лог мы также проверяем, есть ли какой-то результат вызова метода. Если результат не пустой, он также выводится в консоль.
С описанием аспекта мы закончили. Осталось создать в папке /resources/META-INF мета-файл aop-ajc.xml, в котором следует указать пульт до нового класса:
Теперь когда мы запускаем тест, в консоль выводятся все шаги, которые попадают в отчет Allure. Параллельно мы получаем сводку по результатам.
У меня на проектах задача логирования в консоль решается через встроенный механизм Selenide, который позволяет добавлять listener-ы на различные события. А аспекты используются для других задач. Например, для логирования HTTP-запросов из созданных через OpenAPI-генератор Swagger-клиентов. Также с помощью AOP я добавляю текущее время и дату к названиям тестовых шагов в отчетах Allure. Так удобнее утром разбирать ночные прогоны. Еще один вариант использования — логирование SQL-запросов в БД.
Механизм повторов
Аспекты позволяют реализовать необходимое количество повторов тестовых шагов.
Предположим, некий тестовый шаг со специальной аннотацией не всегда проходит с первого раза. Мы хотели бы, чтобы при неудаче у нас было еще 2-3 попытки. И если хотя бы одна из них выполнилась корректно, шаг считался бы зеленым.
В качестве примера я сделал шаг checkRandom(), где мы генерируем случайное число от 0 до 9 и проверяем, что оно равно desiredValue:
public class CommonSteps < @Step("Проверяем, что случайное число = ") @WithRetries(20) public static void checkRandom(int desiredValue) < Random random = new Random(); int i = random.nextInt(10); MatcherAssert.assertThat( "Неправильный результат", i, Matchers.equalTo(desiredValue) ); >>
Без аспекта тест ожидаемо падает (с соответствующей вероятностью).
Чтобы обеспечить повторы, я создал новую аннотацию @WithRetries. По умолчанию методы, отмеченные этой аннотацией, повторяются 3 раза:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface WithRetries
Также я создал класс:
@Aspect public class WithRetriesAspect < private static final ThreadLocalprocessingWrapper = ThreadLocal.withInitial(() -> false); private static final Logger log = LoggerFactory.getLogger(WithRetriesAspect.class); public static Boolean isProcessing() < return processingWrapper.get(); >. >
В отличие от предыдущего примера, здесь я использую pointcut @Around. Это позволяет полностью контролировать выполнение методов, отмеченных аннотацией @WithRetries.
@Around("@annotation(org.example.aop.annotations.WithRetries) && execution(* *(..))") public Object handleRetries(final ProceedingJoinPoint joinPoint) throws Throwable < processingWrapper.set(true); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); WithRetries annotation = method.getAnnotation(WithRetries.class); int retryCount = annotation.value(); Throwable storedException = null; Object result = null; boolean processed = false; int i = 0; while (!processed && i < retryCount) < try < result = joinPoint.proceed(); processed = true; >catch(Throwable throwable) < log.warn("Попытка №" +i+":\r\n"+throwable.toString()); storedException = throwable; >i++; >
Разберем, что здесь происходит.
Внутри Advice я получаю из аннотации количество попыток выполнения и пытаюсь исполнить метод. Если возникает исключение, я его перехватываю здесь же и сохраняю в переменную storedException. В конце я проверяю, по какой причине мы вышли из цикла — получили результат или переполнился счетчик попыток. Если какой-то результат есть, я возвращаю его, а в ином случае выкидываю исключение.
processingWrapper.set(false); if(!processed) < assert storedException != null; throw storedException; >return result;
Подключается аспект точно также в файле /resources/META-INF/aop-ajc.xml
Поглощение исключений
Еще один интересный пример возможностей АОП — это обработка исключений таким образом, чтобы при их появлении появлялось предупреждение, но тест не останавливался.
Подход аналогичный предыдущим двум примерам. Я создаю аспект:
@Aspect public class MatcherAssertAspect
Но pointcut здесь немного сложнее.
Такой pointcut сработает на все методы, которые называются assertThat:
@Pointcut("execution(* assertThat(..))") void inMethods()
Следующий pointcut — на все методы, которые расположены в классе org.hamcrest.MatcherAssert:
@Pointcut("execution(* org.hamcrest.MatcherAssert.*(..))") void inMatcherAssert()
А этот pointcut сработает на все публичные методы:
@Pointcut("execution(public * *(..))") void anyPublic()
После объединения всех pointcut-ов у нас остаются все публичные методы с названием assertThat внутри класса org.hamcrest.MatcherAssert.
Внутри аспекта используем тот же Advice @Arround:
@Around("anyPublic() && inMatcherAssert() && inMethods()") public Object handleAssert(final ProceedingJoinPoint joinPoint) throws Throwable < Object result = null; try < result = joinPoint.proceed(); >catch (Throwable throwable) < if (WithRetriesAspect.isProcessing()) < throw throwable; >else < log.warn(throwable.toString()); >> return result; >
Здесь у меня добавлена синхронизация с описанным выше аспектом, обрабатывающим повторы.
Простейший тест для проверки выглядит следующим образом:
@Test void errorsSkippingTest()
Перед запуском также подключаем класс в файле /resources/META-INF/aop-ajc.xml
Используя повторы и поглощение исключений можно обрабатывать самые неприятные тесты — Flaky-тесты. Ты запускаешь их несколько раз подряд с одними и теми же тестовыми данными в одном и том же окружении, а результат оказывается случайным (то зеленые, то красные). Они не несут в себе совершенно никакой полезной информационной нагрузки, но мешают получению данных о работоспособности тестируемого продукта.
