TestNg失敗重跑—解決使用 dataProvider 引數化用例次數衝突問題

tester_ggf發表於2020-07-20

問題背景

在使用 testng 執行 UI 自動化用例時,由於 UI自動化的不穩定性,我們在測試的時候,往往會加上失敗重跑機制。在不使用 @DataProvider 提供用例引數化時,是不會有什麼問題,如果使用了的話就會出現多條用例都是失敗時,重跑機制只會執行第一次失敗的用例,其他用例的失敗重跑就不執行了。

如下:提供的兩組引數都是失敗時!(重跑的次數設定為2次)

從上圖中可以看出,第一次失敗的用例有重跑了2次,第二次失敗的用例就沒有重跑2次。

TestNg重跑機制程式碼實現

TestNg提供的重跑機制,實現思路如下:

  • 建立一個實現類,實現 IRetryAnalyzer 介面,重寫該介面的 retry方法,定義失敗重跑的規則。
  • 建立一個實現類,實現 IAnnotationTransformer 介面,重寫介面 transform 方法,用於監聽所有的@Test 註解的測試方法。
  • 在 TestNg 的 xml 檔案中配置監聽。

建立 IRetryAnalyzer 實現類

package com.ggf.testng.listener;

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;

/**
 * @Description: 失敗重試方法
 *  用來監聽用例的執行情況,如果斷言失敗,或者程式碼出現錯誤了
 *  都會被這個方法進行捕獲到,然後通過返回值來判斷是否進行重試。
 * @Author: ggf
 * @Date: 2020/07/20
 */
public class TestngRetry implements IRetryAnalyzer {
    /**
     * 最大的重跑次數
     * 設定用例最多重跑多少次
     */
    private int maxRetryCount = 2;
    /**
     * 當前的重跑的次數
     */
    private int currentRetryCount = 1;
    /**
     * 複寫 IRetryAnalyzer 的方法,所有的用例執行完後的結果都會
     * 封裝到這個物件ITestResult 傳入到 retry.xml 方法,通過這個方法
     * 返回值來判斷是否需要重新執行用例。false :不重跑  true:重跑。
     * @param iTestResult
     * @return
     */
    @Override
    public boolean retry(ITestResult iTestResult) {
        //如果retry方法返回為true--》執行重試機制
        //如果返回是為false --》不會去執行重試
        //什麼時候會執行到這裡??條件-->測試用例執行失敗
        if(currentRetryCount <= maxRetryCount){
            //執行了一次重試機制之後,我們就加1
            //如果執行第一次重試機制-->用例執行成功了,用例的結果是pass的
            //如果執行第一次重試機制-->用例執行成功了,第二次重試機制不會執行
            System.out.println("重跑第【"+currentRetryCount+"】次!");
            currentRetryCount++;
            return true;
        }else{
            return false;
        }
    }
}

建立 IAnnotationTransformer 實現類

package com.ggf.testng.listener;

import org.testng.IAnnotationTransformer;
import org.testng.IRetryAnalyzer;
import org.testng.annotations.ITestAnnotation;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

/**
 * @Description:
 * 由於在使用重跑機制的時候需要在每個用例@Test註解新增 retryAnalyzer 屬性。
 * 如果用例量過大的話,非常的麻煩,所以我們引入 testng 提供的監聽器類:IAnnotationTransformer
 * 通過這個監聽器類來實現,動態的修改@Test註解屬性,我們就可以統一給 @Test 註解動態加上屬性retryAnalyzer 值。
 * @Author: ggf
 * @Date: 2020/07/20
 */
public class RetryListener implements IAnnotationTransformer {
    @Override
    public void transform(ITestAnnotation iTestAnnotation, Class aClass, Constructor constructor, Method method) {
        //1、拿到@test註解的retryAnalyzer屬性物件
        IRetryAnalyzer iRetryAnalyzer = iTestAnnotation.getRetryAnalyzer();

        //2、如果@test的retryAnalyzer屬性沒有設定,iRetryAnalyzer-->null
        if(iRetryAnalyzer == null){
            iTestAnnotation.setRetryAnalyzer(TestngRetry.class);
        }
    }
}

xml 檔案配置監聽器

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="retry">
    <test name="retryTest">
        <classes>
            <class name="com.ggf.testng.listener.RetryDemo"></class>
        </classes>
    </test>

    <listeners>
        <!--失敗重試監聽器-->
        <listener class-name="com.ggf.testng.listener.RetryListener"></listener>
    </listeners>

</suite>

測試類

package com.ggf.testng.listener;

import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * @Description:
 * @Author: ggf
 * @Date: 2020/07/20
 */
public class RetryDemo {
    @Test(dataProvider = "data")
    public void testRetry(String data1, String data2) {
        // 斷言兩個引數是否一樣
        Assert.assertEquals(data1, data2);
    }

    @DataProvider
    public Object[][] data() {
        // 提供兩組測試引數
        return new Object[][]{{"111","123"}, {"123", "1234"}};
    }
}

失敗用例重跑問題重現

對於一個使用了dataProvider的用例,因為這個用例是一個標記為@Test的方法,會共用TestngRetrycurrentRetryCount,即整個方法的所有引數化用例,總共只會重跑 2 次。例如一個引數化用例有 2 組引數,如果全部正確,每個用例只會輸出一次:

Test1: success
Test2: success

如果兩組引數的用例都失敗了,對於第一組引數是會重跑2次的(程式碼設定的是2次,不包含第一次),到了第二組引數就不會繼續重跑了,因為currentRetryCount在第一組引數用例跑完,當前值就為 3 了,這個時候不滿足重跑條件,第二組引數用例失敗後就不會重跑了。

Test1: failed -> skipped
Test1: failed -> skipped
Test1: failed
Test2: failed
Test2: failed

至於這裡的 Test2 為什麼會跑 2 次,沒搞明白,正常來說應該是跑一次的,因為程式中 if(currentRetryCount <= maxRetryCount) ` 滿足才會重跑,但是這裡是 false 所以不會重跑才對。如果有大神們知道為什麼,希望不吝賜教,留個言。。。

問題解決方案

要解決上述的問題,需要在每組引數化用例結束(無論成功,失敗)後,重置 currentRetryCount 的值,讓當前的次數保持在初始化的狀態。

在實現類 TestngRetry 中加上一個重置的方法,如下:

package com.ggf.testng.listener;

import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;

/**
 * @Description: 失敗重試方法
 *  用來監聽用例的執行情況,如果斷言失敗,或者程式碼出現錯誤了
 *  都會被這個方法進行捕獲到,然後通過返回值來判斷是否進行重試。
 * @Author: ggf
 * @Date: 2020/07/20
 */
public class TestngRetry implements IRetryAnalyzer {
    /**
     * 最大的重跑次數
     * 設定用例最多重跑多少次
     */
    private int maxRetryCount = 2;
    /**
     * 當前的重跑的次數
     */
    private int currentRetryCount = 1;
    /**
     * 複寫 IRetryAnalyzer 的方法,所有的用例執行完後的結果都會
     * 封裝到這個物件ITestResult 傳入到 retry.xml 方法,通過這個方法
     * 返回值來判斷是否需要重新執行用例。false :不重跑  true:重跑。
     * @param iTestResult
     * @return
     */
    @Override
    public boolean retry(ITestResult iTestResult) {
        //如果retry方法返回為true--》執行重試機制
        //如果返回是為false --》不會去執行重試
        //什麼時候會執行到這裡??條件-->測試用例執行失敗
        if(currentRetryCount <= maxRetryCount){
            //執行了一次重試機制之後,我們就加1
            //如果執行第一次重試機制-->用例執行成功了,用例的結果是pass的
            //如果執行第一次重試機制-->用例執行成功了,第二次重試機制不會執行
            System.out.println("重跑第【"+currentRetryCount+"】次!");
            currentRetryCount++;
            return true;
        }else{
            return false;
        }
    }

    /**
     * 用於重置失敗重跑時的次數,還原到初始化的值
     * 如果專案中是使用dataProvider註解來提供用例測試資料引數化的,
     * 那麼每個@Test執行的時候都會共有重跑的次數。
     * 例如:一個引數化用例有 3 組引數,如果全部正確,結果是:全部通過
     * 如果第一組引數,第一次失敗(第二次成功,這裡就用掉了一次重跑的次數,currentRetryCount 就+1了)
     * 接著第二組引數每次執行都失敗,這個時候currentRetryCount=2, 那麼第二組引數也就只會執行一次重跑。
     */
    public void reset() {
        currentRetryCount = 1;
    }
}


再新建一個監聽類 TestngListener , 繼承 TestListenerAdapter 類,並重寫 onTestSuccessonTestFailure 方法:

package com.ggf.testng.listener;

import org.testng.ITestContext;
import org.testng.ITestNGMethod;
import org.testng.ITestResult;
import org.testng.TestListenerAdapter;

import java.util.Iterator;

/**
 * @Description:
 * @Author: ggf
 * @Date: 2020/07/20
 */
public class TestngListener extends TestListenerAdapter {
    @Override
    public void onTestSuccess(ITestResult iTestResult) {
        super.onTestSuccess(iTestResult);
        TestngRetry testngRetry = (TestngRetry)iTestResult.getMethod().getRetryAnalyzer();
        testngRetry.reset();
    }

    @Override
    public void onTestFailure(ITestResult iTestResult) {
        super.onTestFailure(iTestResult);
        // 每次dataProvider中的引數跑完一次,就重置一次當前的重跑次數,恢復到預設值,保證每個失敗的用例都能重跑設定的次數。
        TestngRetry testngRetry = (TestngRetry)iTestResult.getMethod().getRetryAnalyzer();
        testngRetry.reset();
    }
}

再把新建的TestngListener 監聽類,配置到 xml 檔案中:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >

<suite name="retry">
    <test name="retryTest">
        <classes>
            <class name="com.ggf.testng.listener.RetryDemo"></class>
        </classes>
    </test>

    <listeners>
        <!--失敗重試監聽器-->
        <listener class-name="com.ggf.testng.listener.RetryListener"></listener>
        <!--用例執行結果監聽-->
        <listener class-name="com.ggf.testng.listener.TestngListener"></listener>
    </listeners>

</suite>

再次執行的結果:

以上就是對於解決使用了dataProvider用例中的每一個引數化用例,在不重置的情況下,用例重跑次數共用的問題。

最後我們加上重置操作後,失敗的用例都會重跑跑 2 次,無論最後成功還是失敗,都會重置 TestngRetry 中的currentRetryCount 以保證下一個引數化用例開始時,currentRetryCount 為初始狀態。

參考文章:https://ntflc.com/2018/10/18/TestNg-Retry-Failed-Tests-with-DataProvider/

相關文章