<style>.lazy{display:none}</style>Создание повторных запусков в автоматизированных тестах

Создание повторных запусков в автоматизированных тестах

Перевод статьи «A Primer to Building Retry in Automated Tests».

Эта статья посвящена написанию отказоустойчивых (по принципу “fail-safe”) автотестов. Если судить по опыту, можно выделить одно: во время тестирования с продуктом могут возникать абсолютно разные непредвиденные ситуации. Самая частая проблема – это случайные, непостоянные ошибки, например сетевые сбои, тайм-ауты методов, повышенная нагрузка на связанные сервисы и т. д. Важно, чтобы такие ошибки обрабатывались систематически и учитывалось, что ошибки могут разрешиться сами собой при повторном перезапуске того же действия.

Подпишитесь на наш ТЕЛЕГРАМ КАНАЛ ПО АВТОМАТИЗАЦИИ ТЕСТИРОВАНИЯ

Обратная сторона

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

Если подумать, это ничем не отличается от подбрасывания монеты – в итоге выпадет либо орел, либо решка. Хотя количество проваленных тестов может уменьшаться или даже увеличиваться при повторном перезапуске, нет гарантированного способа, чтобы такие непостоянные тесты всегда исправлялись. Через какое-то время я понял, что это все было лишь совпадением, когда тесты успешно выполнялись, но в то же время были подвержены ошибкам. Им просто не хватало стабильности.

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

В качестве примечания, вот один из примеров кода, демонстрирующий правильную конфигурацию повторной попытки в CucumberJS.

Исправьте слабые места

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

В этой статье я использую библиотеку Java failsafe вместе с Selenium, однако эти принципы применимы и в других языках и фреймворках.

implementation 'dev.failsafe:failsafe:3.3.2'

Смотрите также: “Cucumber в Cypress: Пошаговое руководство”

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

Пользователь переходит к созданию новой учетной записи.

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

Пользователь создает учетную запись и проверяет сообщение: ‘Спасибо за …’.

Если эти кейсы реализовывать на Java, понадобятся функциональные интерфейсы: Runnable и Supplier соответственно.

В следующей иллюстрации кода метод retryStep принимает Runnable, а также два других параметра:

  1. runnable – это лямбда-выражение, которое должно быть перезапущено в рамках шагов сценария. Стоит также отметить, что оно не возвращает значения вызывающему его шагу.
  2. maxRetry – количество перезапусков для определенного шага. Обратите внимание, что первая попытка не засчитывается.
  3. delaySeconds – время ожидания перед следующей попыткой.
default void retryStep(CheckedRunnable runnable, int maxRetry, int delaySeconds) {
  RetryPolicy<Object> policy = policyBuilder.withMaxRetries(maxRetry)
      .withDelay(Duration.ofSeconds(delaySeconds))
      .handle(exceptionsList)
      .onRetry(e -> log.warn(sf("attempting retry#: %d", e.getAttemptCount())))
      .onFailure(
          e -> log.error(sf("attempts: %d have failed", e.getExecutionCount())))
      .build();
  Failsafe.with(policy).run(runnable);
}

Более того, в следующем примере, метод getWithRetryStep принимает Supplier вместо Runnable, а также два других параметра:

  1. supplier – это лямбда-выражение, которое должно быть повторно использовано в рамках шагов сценария. Отличие от runnable в том, что supplier возвращает значение вызывающему его шагу.
  2. maxRetry – количество попыток, которое необходимо повторить для определенного шага. Исключает первую попытку.
  3. delaySeconds – время ожидания перед следующей попыткой.
default Object getWithRetryStep(CheckedSupplier<Object> supplier, int maxRetry,
    int delaySeconds) {
  RetryPolicy<Object> policy = policyBuilder.withMaxRetries(maxRetry)
      .withDelay(Duration.ofSeconds(delaySeconds))
      .handle(exceptionsList)
      .handleResult(null)
      .onRetry(e -> log.warn(sf("attempting retry#: %d", e.getAttemptCount())))
      .onFailure(
          e -> log.error(sf("attempts: %d have failed", e.getExecutionCount())))
      .build();
  return Failsafe.with(policy).get(supplier);
}

Далее я обернул реализацию шага теста в лямбда-выражение и передал его в качестве параметра методу retryStep вместе с двумя другими параметрами конфигурации retry.

@When("User navigates to create new customer account")
public void userNavigatesToCreateNewCustomerAccount() {
  retryStep(() -> homePage.navigateToNewAccountPage(),
      MAX_RETRIES_DEFAULT,
      MAX_DELAY_SECONDS_DEFAULT);
}

Следующий кейс немного отличается, поскольку мы проверяем результат, который возвращает метод getWithRetryStep на соответствие ожидаемому значению.

@Then("User creates an account and verifies the message: {string}")
public void userCreatesAnAccountAndVerifiesTheMessage(String alertText) {
  boolean IsAlertMessageDisplayed = (boolean) getWithRetryStep(() -> {
        createNewAccountPage.createAnAccount();
        return createNewAccountPage.IsAlertMessageDisplayed(alertText);
      },
      MAX_RETRIES_DEFAULT,
      MAX_DELAY_SECONDS_DEFAULT
  );
  Assertions.assertThat(IsAlertMessageDisplayed)
      .as("Assert that alert is displayed")
      .isTrue();
}

По итогу, лучше иметь две вариации метода retry: одну, которая не возвращает значение, и другую, которая возвращает значение вызывающему ее методу из лямбды. Кроме того,  вместо failsafe  можно рассмотреть и другие библиотеки: resilience4j, spring-retry.

Уверен, что вы найдете это полезным. Также оставляю репозиторий, содержащий весь код из статьи.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *