Реализация автоматического перезапуска failed-тестов в текущей сборке и преодоление сопутствующих бед

от автора

В данной статье речь пойдет об использовании фреймворка testNG, а конкретно — о реализованных в нем и довольно редко используемых интерфейсах: IRetryAnalyzer, ITestListener, IReporter. Но обо всем по порядку.

Вечной проблемой каждого тестировщика при запуске автотестов является “падение” отдельных сценариев от запуска к запуску рандомно. И речь идет не о падении наших тестов по объективным причинам (т.е. действительно имеет место ошибка в работе тестируемого функционала, или же сам тест написан не корректно), а как раз о тех случаях, когда после перезапуска ранее проваленные тесты чудом проходят. Причин такого рандомного падения может быть масса: отвалился интернет, перегрузка CPU / отсутствие свободной RAM на устройстве, таймаут и др. Вопрос — как исключить или хотя бы уменьшить количество таких не объективно проваленных тестов?

Для меня данный челлендж возник при следующих обстоятельствах:

1) текущее приложение автотестов было решено разместить на сервере (CI);
2) реализация мультипоточности в проекте превратилась из желания в mustHave (в виду необходимости сокращения времени регрессионного тестирования сервиса).

Второму пункту лично я был очень рад, так как считаю, что любой процесс, который может длиться меньшее количество времени — обязательно должен поступать именно таким образом (будь то прохождение автотеста или очередь на кассе в супермаркете: чем быстрее мы можем завершить эти процессы, тем больше времени у нас остается для занятий чем-то действительно интересным). Так вот, разместив наши тесты на сервере (тут нам помогли админы и их знание jenkins) и запустив их в потоках (тут уже помогла наша усидчивость и эксперименты с testng.xml), мы получили сокращение времени прохождения тестов из 100 минут до 18, но одновременно мы получили прирост в проваленных тестах >2 раза. Поэтому к первым двум пунктам добавился следующий (собственно, сам челлендж, которому и посвящена эта статья):

3) реализовать перезапуск проваленных тестов в процессе одной сборки.

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

Реализовать перезапуск проваленного теста testng позволяет из коробки, благодаря интерфейсу IRetryAnalyzer. Данный интерфейс предоставляет нам boolean-метод retry, который и отвечает за перезапуск теста, в случае возврата им true, или же отсутствие перезапуска при false. Передать в данный метод нам нужно результат нашего теста (ITestResult result).

Теперь проваленные тесты начали перезапускаться, но была выявлена следующая неприятная особенность: все провальные попытки прохождения теста неминуемо попадают в отчетники (так как реально ваш тест, пусть один и тот же, проходит несколько раз подряд — по нему поступает такое же количество результатов, которые честно попадают в отчет). Возможно, некоторым тестировщикам данная проблема покажется надуманной (особенно, если отчет вы никому не показываете, не предоставляете его техлидам, менеджерам и заказчикам). В таком случае, действительно, можно пользоваться maven-surefire-report-plugin и периодически злиться, ломая глаза, чтобы понять, провален ваш тест или таки нет.

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

Рассматривались варианты парсинга html-отчета для удаления дублирующихся проваленных тестов. Также предлагали мержить результаты нескольких отчетов в 1 конечный. Подумав, что костыльные решения могут аукнуться нам, когда с очередным обновлением report-плагинов будет изменена структура их html/xml отчетов, было принято решение реализовать создание собственного кастомного отчета. Единственный минус такого решения — время на его разработку и тестирование. Плюсов я увидел гораздо больше, и главный из них — гибкость. Отчет можно сформировать так, как нужно или нравится вам. Всегда можно добавить дополнительные параметры, поля, метрики.

Итак, было понятно, в каком месте в отчет будут складываться проваленные тесты — это блок retry-метода, в котором количество попыток перезапуска тестов уже было исчерпано. Далее определились с тем, откуда складывать успешные. Интерфейс ITestListener. Из семи методов данного интерфейса нам идеально подошел onTestSuccess, т.к. успешные тесты всегда заходят в данный метод. Итого, у нас есть две точки в нашем приложении, откуда к нам в отчет будут складываться успешные и проваленные тесты.

Следующий вопрос: в какой момент дернуть наш отчет, чтобы к этому времени все тесты были завершены. Тут на помощь приходит следующий интерфейс — IReporter и его метод generateReport.

Итак, теперь у нас есть:

— метод, откуда мы будем укладывать в отчет успешные тесты;
— аналогичный метод, только под проваленные тесты;
— метод, который знает, когда все тесты завершены и может “дернуть” наш генератор самого отчета (которого пока нет).

Для работы с html в java была выбрана библиотека gagawa. Тут вы можете сверстать отчет так, как вам захочется, отталкиваясь как от имеющихся у вас параметров, так и от требуемых, на ваше усмотрение, метрик для отчета. После — подключить в проект простенькую css-ку для лучшей визуализации нашего отчета и работы со стилями.

Теперь непосредственно о реализации данных фич у меня (комментарии для читабельности).

RetryAnalyzer:

Переменные retryCount и retryMaxCount позволяют управлять количеством требуемых перезапусков в случае провала теста. В остальном, считаю код вполне читабельным.

public class RetryAnalyzer implements IRetryAnalyzer {     private int retryCount = 0;     private int retryMaxCount = 3;      // решаем, требует ли тест перезапуска     @Override     public boolean retry(ITestResult testResult) {         boolean result = false;         if (testResult.getAttributeNames().contains("retry") == false) {             System.out.println("retry count = " + retryCount + "\n" +"max retry count = " + retryMaxCount);             if(retryCount < retryMaxCount){                 System.out.println("Retrying " + testResult.getName() + " with status "                         + testResult.getStatus() + " for the try " + (retryCount+1) + " of "                         + retryMaxCount + " max times.");                  retryCount++;                 result = true;             }else if (retryCount == retryMaxCount){                 // тут будем складывать в отчет неуспешные тесты                 // получаем все необходимые параметры теста                 String testName = testResult.getName();                 String className = testResult.getTestClass().toString();                 String resultOfTest = resultOfTest(testResult);                 String stackTrace = testResult.getThrowable().fillInStackTrace().toString();                 System.out.println(stackTrace);                 // и записываем в массив тестов                 ReportCreator.addTestInfo(testName, className, resultOfTest, stackTrace);             }         }         return result;     }     // простенький метод для записи в результат теста  saccess / failure     public String resultOfTest (ITestResult testResult) {         int status = testResult.getStatus();         if (status == 1) {             String TR = "Success";             return TR;         }         if (status == 2) {             String TR = "Failure";             return TR;         }         else {             String unknownResult = "not interested for other results";             return unknownResult;         }     } } 

TestListener

Тут ловим успешные тесты, как вы уже знаете.

public class TestListener extends TestListenerAdapter {      // успешные всегда заходят в onSuccess юзаем его     @Override     public void onTestSuccess(ITestResult testResult) {         System.out.println("on success");         // в этом методе складываем в массив  успешные тесты, определяем их параметры         String testName = testResult.getName();         String className = testResult.getTestClass().toString();         String resultOfTest = resultOfTest(testResult);         String stackTrace = "";         ReportCreator.addTestInfo(testName, className, resultOfTest, stackTrace);     }      // еще 1 простенький метод для записи в результат теста  saccess / failure     public String resultOfTest (ITestResult testResult) {         int status = testResult.getStatus();         if (status == 1) {             String TR = "Success";             return TR;         }         if (status == 2) {             String TR = "Failure";             return TR;         }         else {             String unknownResult = "not interested for other results";             return unknownResult;         }     } } 

Reporter

Дергаем наш отчет, т.к. понимаем, что все тесты уже завершены.

public class Reporter implements IReporter {  // метод, который стартует после окончания всех тестов и дергает наш getReport для получения html в string     @Override     public void generateReport(List<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {         PrintWriter saver = null;         try {              saver = new PrintWriter(new File("report.html"));              saver.write(ReportCreator.getReport());         } catch (FileNotFoundException e) {             e.printStackTrace();         } finally {             if (saver != null) {                 saver.close();             }         }     } } 

ReportCreator

Сам генератор нашего html-отчета.

public class ReportCreator {     public static Document document;     public static Body body;     public static ArrayList<TestData> list = new ArrayList<TestData>();      // изображение для хедера отчета     public static void headerImage (){          Img headerImage = new Img("", "src/main/resources/baad.jpeg");         headerImage.setCSSClass("headerImage");         body.appendChild(headerImage);      }      // общий блок отчета (все запущенные тесты: успех + неуспех)     public static void addTestReport(String className, String testName, String status) {          if (status == "Failure"){             Div failedDiv = new Div().setCSSClass("AllTestsFailed");             Div classNameDiv = new Div().appendText(className);             Div testNameDiv = new Div().appendText(testName);             Div resultDiv = new Div().appendText(status);             failedDiv.appendChild(classNameDiv);             failedDiv.appendChild(testNameDiv);             failedDiv.appendChild(resultDiv);             body.appendChild(failedDiv);         }else{             Div successDiv = new Div().setCSSClass("AllTestsSuccess");             Div classNameDiv = new Div().appendText(className);             Div testNameDiv = new Div().appendText(testName);             Div resultDiv = new Div().appendText(status);             successDiv.appendChild(classNameDiv);             successDiv.appendChild(testNameDiv);             successDiv.appendChild(resultDiv);             body.appendChild(successDiv);         }     }      // тут записываем в отчет основные метрики рана (общее кол-во тестов, кол-во успешных и неуспешных тестов)     public static void addCommonRunMetrics (int totalCount, int successCount, int failureCount) {         Div total = new Div().setCSSClass("HeaderTable");         total.appendText("Total tests count: " + totalCount);         Div success = new Div().setCSSClass("HeaderTable");         success.appendText("Passed tests: " + successCount);         Div failure = new Div().setCSSClass("HeaderTable");         failure.appendText("Failed tests: " + failureCount);         body.appendChild(total);         body.appendChild(success);         body.appendChild(failure);     }      // тут формируем отдельный блок с упавшими тестами в хедер отчета для наглядности     public static void addFailedTestsBlock (String className, String testName, String status) {         Div failed = new Div().setCSSClass("AfterHeader");         Div classTestDiv = new Div().appendText(className);         Div testNameDiv = new Div().appendText(testName);         Div statusTestDiv = new Div().appendText(status);         failed.appendChild(classTestDiv);         failed.appendChild(testNameDiv);         failed.appendChild(statusTestDiv);         body.appendChild(failed);     }      // тут формируем отдельный блок в футтер отчета со стектрейсами зафейленных тестов     public static void addfailedWithStacktraces (String className, String testName, String status, String stackTrace) {         Div failedWithStackTraces = new Div().setCSSClass("Lowest");         failedWithStackTraces.appendText(className + " " + testName + " " + status + "\n");         Div stackTraceDiv = new Div();         stackTraceDiv.appendText(stackTrace);         body.appendChild(failedWithStackTraces);         body.appendChild(stackTraceDiv);     }      // тут складываем в arraylist наши тесты с нужными для отчета параметрами     public static void addTestInfo(String testName, String className, String status, String stackTrace) {         TestData testData = new TestData();         testData.setTestName(testName);         testData.setClassName(className);         testData.setTestResult(status);         testData.setStackTrace(stackTrace);         list.add(testData);     }        // итоговый метод, который вызывается после прохождения всех тестов для формирования html-отчета     public static String getReport() {         document = new Document(DocumentType.XHTMLTransitional);         Head head = document.head;         Link cssStyle= new Link().setType("text/css").setRel("stylesheet").setHref("src/main/resources/site.css");         head.appendChild(cssStyle);         body = document.body;          // тут будет общее кол-во тестов         int totalCount = list.size();         // тут формируем массив зафейленных тестов         ArrayList failedCountArray = new ArrayList();         for (int f=0; f < list.size(); f++) {             if (list.get(f).getTestResult() == "Failure") {                 failedCountArray.add(f);             }         }         int failedCount = failedCountArray.size();         // получаем кол-во успешных тестов         int successCount = totalCount - failedCount;         // записываем в    html нашу картинку в хедере         headerImage();         // записываем в    html основные метрики         addCommonRunMetrics(totalCount, successCount, failedCount);         // записываем в html зафейленные тесты         for (int s = 0; s < list.size(); s++){             if (list.get(s).getTestResult() == "Failure"){                 addFailedTestsBlock(list.get(s).getClassName(), list.get(s).getTestName(), list.get(s).getTestResult());             }         }         // проверяем, что массив с тестами всего рана не пуст         if(list.isEmpty()){             System.out.println("ERROR: TEST LIST IS EMPTY");             return "";         }         // сортируем в нашем массиве тесты по классам (для красивого отсортированного отчета) + записываем их в html         String currentTestClass = "";         ArrayList constructedClasses = new ArrayList();         for(int i=0; i < list.size();i++){             currentTestClass = list.get(i).getClassName();             //проверка создали ли мы хтмл для текущего класса             boolean isClassConstructed=false;             for(int j=0;j<constructedClasses.size();j++){                 if(currentTestClass.equals(constructedClasses.get(j))){                     isClassConstructed=true;                 }             }             if(!isClassConstructed){                 for (int k=0;k<list.size();k++){                     if(currentTestClass.equals(list.get(k).getClassName())){                         addTestReport(list.get(k).getClassName(), list.get(k).getTestName(),list.get(k).getTestResult());                     }                 }                 constructedClasses.add(currentTestClass);             }         }         // получаем необходимые параметры зафейленных тестов + записываем их в html         for (int z = 0; z < list.size(); z++){             if (list.get(z).getTestResult() == "Failure"){                 addfailedWithStacktraces(list.get(z).getClassName(), list.get(z).getTestName(), list.get(z).getTestResult(), list.get(z).getStackTrace());             }         }         return document.write();     }      // наш класс теста с необходимыми для отчета параметрами + getter'ы / setter'ы     public static class TestData{         String testName;         String className;         String testResult;         String stackTrace;          public TestData() {}          public String getTestName() {             return testName;         }          public String getClassName() {             return className;         }          public String getTestResult() {             return testResult;         }          public String getStackTrace() {             return stackTrace;         }          public void setTestName(String testName) {             this.testName = testName;         }          public void setClassName(String className) {             this.className = className;         }          public void setTestResult(String testResult) {             this.testResult = testResult;         }          public void setStackTrace(String stackTrace) {             this.stackTrace = stackTrace;         }     } } 

Сам класс с тестами

@Listeners(TestListener.class) // необходимо навесить данную аннотацию над классом тестов, чтобы они анализировались TestListener public class Test {     private static WebDriver driver;      @BeforeClass     public static void init () {         driver = new FirefoxDriver();         driver.get("http://www.last.fm/ru/");     }      @AfterClass     public static void close () {         driver.close();     }      @org.testng.annotations.Test (retryAnalyzer = RetryAnalyzer.class) // данная аннотация необходима для подключения RetryAnalyzer к конкретному тесту     public void findLive () {         driver.findElement(By.cssSelector("[href=\"/ru/dashboard\"]")).click();     } } 

Также нужно добавить в файл testng.xml следующий тег с указанием пути к классу Reporter:

 <listeners>         <listener class-name= "retry.Reporter" />     </listeners> 

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

image

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

Возможно не достаточно изящное или простое — в таком приветствую критику в комментариях. Для себя главным плюсом данного набора я вижу универсальность: переиспользовать разработку можно будет на любом java+testng-проекте в дальнейшем.

Мой github с данным проектом.

ссылка на оригинал статьи http://habrahabr.ru/post/272643/


Комментарии

Добавить комментарий

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