現在のアセンブリで失敗したテストの自動再起動の実装および関連するトラブルの克服

この記事では、testNGフレームワークの使用、具体的にはそれに実装され、めったに使用されないインターフェースであるIRetryAnalyzer、ITestListener、IReporterについて説明します。 しかし、まず最初に。



自動テストを実行する際の各テスターの永遠の問題は、個々のスクリプトが実行からランダムに実行される「落下」です。 そして、これは客観的な理由によるテストの失敗に関するものではありません(つまり、テストされた機能の機能に実際にエラーがあるか、テスト自体が正しく記述されていない)が、再起動後、以前にテストが奇跡的に合格した場合のみです。 このようなランダムなドロップには、多くの理由があります。インターネットが落ちた、CPUの過負荷/デバイスの空きRAMの不足、タイムアウトなどです。問題は、このような客観的に失敗したテストの数を除外するか、少なくとも減らす方法ですか?



私にとって、この課題は次の状況で発生しました。



1)現在の自動テストアプリケーションをサーバー(CI)に配置することが決定されました。

2)プロジェクトでのマルチスレッドの実装は、mustHaveに変わりました(サービスの回帰テストの時間を短縮する必要性を考慮して)。



第二に、私は個人的に非常に幸せでした。なぜなら、時間のかからないプロセスであれば、間違いなくそれを行うべきだと思うからです(スーパーマーケットでのセルフテストまたは精算ラインを通過するかどうか:本当に面白いことをする時間が残っています)。 したがって、サーバーにテストを配置し(管理者とジェンキンの知識が役立ちました)、ストリームで実行することで(ここでのtestng.xmlの忍耐力と実験が役立ちました)、テスト時間を100分から18分に短縮できましたが、同時に、失敗したテストが2倍に増加しました。 そのため、最初の2つのポイント(実際には、この記事で取り上げる課題自体)に次の点が追加されました。



3) 1つのアセンブリのプロセスで失敗したテストの再起動を実装します。



3番目の段落のボリュームとその要件は次第に大きくなりましたが、繰り返しますが、すべての順序についてです。



Testngでは、 IRetryAnalyzerインターフェイスのおかげで、失敗したテストをすぐに再起動することができます。 このインターフェイスはブールメソッドの再試行を提供します。これは、trueが返された場合はテストを再開し、falseが返された場合は再起動を行わないようにします。 このメソッドに渡すには、テストの結果(ITestResult結果)が必要です。



これで、失敗したテストが再開し始めましたが、次の不快な機能が明らかになりました:テストに合格するすべての失敗した試みは、必然的にアンケートに分類されますレポートに入ります)。 おそらく、一部のテスターはこの問題を大げさに見つけます(特に、レポートを誰にも見せない場合、技術専門家、マネージャー、顧客に提供しない場合)。 この場合、実際には、maven-surefire-report-pluginを使用して定期的に怒り、目をつぶってテストが失敗したかどうかを理解できます。



私は明らかに不正なレポートの見通しに適合しなかったため、解決策の検索は継続されました。



重複した失敗したテストを削除するために、htmlレポートを解析するオプションを検討しました。 また、複数のレポートの結果を1つのファイナルにマージすることを提案しました。 レポートプラグインの次の更新でhtml / xmlレポートの構造が変更されると、松葉杖ソリューションが戻ってくると考えて、独自のカスタムレポートの作成を実装することにしました。 このソリューションの唯一の欠点は、その開発とテストの時間です。 さらに多くの利点がありましたが、主なものは柔軟性です。 必要に応じて、または好きなようにレポートを生成できます。 いつでも追加のパラメーター、フィールド、メトリックを追加できます。



そのため、失敗したテストがレポートのどこに追加されるかは明確でした。これは、テストを再開するための試行回数がすでに使い果たされた再試行メソッドのブロックです。 次に、成功したものをどこに置くかを決めました。 インターフェイスITestListener 。 このインターフェイスの7つのメソッドのうち、onTestSuccessが理想的でした。 成功したテストは常にこのメソッドに入ります。 合計で、アプリケーションには2つのポイントがあり、そこから成功したテストと失敗したテストがレポートに追加されます。



次の質問:どの時点でレポートを取得するので、この時点ですべてのテストが完了しています。 次のインターフェイスが役立ちます-IReporterとそのgenerateReportメソッド。



だから今、私たちは持っています:



-成功したテストをレポートに入れる方法。

-同様の方法、失敗したテストのみ。

-すべてのテストがいつ完了したかを把握し、レポートジェネレーターを「プル」できるメソッド(まだではありません)。



JavaでHTMLを使用するために、 gagawaライブラリが選択されました。 ここで、お持ちのパラメーターと、必要に応じてレポートに必要なメトリックの両方から、必要に応じてレポートを作成できます。 後-簡単なcss-kuをプロジェクトに接続して、レポートを視覚化し、スタイルを操作します。



次に、これらの機能の実装について直接説明します(読みやすくするためのコメント)。



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; } } }
      
      





テストリスナー


すでにご存知のように、ここで成功したテストをキャッチします。



 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; } } }
      
      





記者


レポートを取得します すべてのテストがすでに完了していることを理解しています。



 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(); } }
      
      





また、次のタグをReporterクラスへのパスとともにtestng.xmlファイルに追加する必要があります。



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





最終結果の視覚化は、完全にあなたの裁量に任されています。 たとえば、上記のコードに表示されるレポートは次のようになります。



画像



結論として、一見したところ些細な問題に直面したとき、出口での解決策はもはや些細ではなかったと言いたいと思います。



おそらく、十分にエレガントでも単純でもありません。これで、コメントでの批判を歓迎します。 このセットの主な利点である私自身には、普遍性があります。将来、すべてのjava + testngプロジェクトで開発を再利用できるようになるでしょう。



このプロジェクトの私のgithub



All Articles