В данной статье речь пойдет об использовании фреймворка 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>
Визуализация конечного результата остается полностью на ваше усмотрение. К примеру отчет, который вы видите в коде выше, выглядит вот так:
В заключение хочу сказать, что столкнувшись с довольно тривиальной, на первый взгляд, проблемой, решение на выходе мы получили уже совсем не тривиальное.
Возможно не достаточно изящное или простое — в таком приветствую критику в комментариях. Для себя главным плюсом данного набора я вижу универсальность: переиспользовать разработку можно будет на любом java+testng-проекте в дальнейшем.
Мой github с данным проектом.
ссылка на оригинал статьи http://habrahabr.ru/post/272643/
Добавить комментарий