使用 testrail 作为 testng 测试报告器时的 ID 冲突

ID collision when using testrail as reporter for testng tests

将测试结果从 testgn 发布到 test rail 时,我运行遇到了如何管理唯一测试用例 ID 的问题。

我最初将它们存储在测试方法中,因此每个方法都映射到一个 testrail 测试用例。这一直有效,直到我开始参数化测试方法。

现在假设我想 运行 在四个不同的浏览器上进行单个 selenium 测试,并分别存储结果。我无法将案例 ID 存储在测试方法中,因为四个不同的结果将报告回同一个测试案例。

相反,我决定尝试在 .xml 文件中添加案例 ID 作为参数。这有效,但前提是每个 class 有一个测试方法,否则 testXXX() 和 testYYY() 会为 xml 中的每个条目获得相同的 ID。所以也没有运气。

我正在尝试找到一种方法来存储每个测试的每个版本的案例 ID 运行,而不会牺牲结构(例如放弃参数,或者每个 [=23] 只编写一个测试方法=]).

示例套件如下

<suite name="UL" parallel="tests" thread-count="1" verbose="10">
    <parameter name="env" value="REDACTED"/>
    <parameter name="recordTests" value="1"/>
    <listeners>
    </listeners>
    <test name="UL Tests firefox">
        <classes>
            <class name="tests.selenium_tests.ULTests">
                <parameter name="browser" value="firefox"/>
                <parameter name="case_id" value="1111"/>
            </class>
        </classes>
    </test>
    <test name="UL Tests chrome">
        <classes>
            <class name="tests.selenium_tests.ULTests">
                <parameter name="browser" value="chrome"/>
                <parameter name="case_id" value="1112"/>
            </class>
        </classes>
    </test>
    <test name="UL Tests safari">
        <classes>
            <class name="tests.selenium_tests.ULTests">
                <parameter name="browser" value="bs_safari"/>
            </class>
        </classes>
    </test>
    <test name="UL Tests edge">
        <classes>
            <class name="tests.selenium_tests.ULTests">
                <parameter name="browser" value="bs_edge"/>
            </class>
        </classes>
    </test>
</suite>

这完全取决于您如何在 TCMS 系统中可视化您的 TestCase ID。

如果测试用例代表数据驱动测试,则方法需要稍微不同。

如果一个测试用例代表一个常规测试,那么我相信您已经有了一个可行的解决方案。

这是完成此操作的一种方法。我正在使用 TestNG 7.0.0-beta3(截至今天的最新发布版本)

假设:

  • TCMS 中的测试用例表示实际测试的 "n" 次迭代,被认为是通过 当且仅当 所有迭代都通过,否则它是一个失败。

要遵循的步骤:

  1. 您首先创建一个捕获特定测试的 TCMS(测试用例管理系统)ID 的自定义注释。
  2. 您使用自定义注释对您的 @Test 方法进行注释,以将其绑定到特定的 TCMS 测试用例。
  3. 您现在构建一个自定义侦听器,以确保它能够区分普通测试和数据驱动测试,并post 相应地区分结果。对于数据驱动的测试,他们需要跟踪到目前为止 运行 的所有迭代,然后计算总体结果。

我的博客里也有同样的阐述post here.

这里有一个示例,展示了所有这些操作:

自定义注释如下所示:

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({METHOD, TYPE})
public @interface Tcms {
  String id() default "";
}

侦听器如下所示:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.testng.IInvokedMethod;
import org.testng.IInvokedMethodListener;
import org.testng.ITestResult;

public class TestRailReporter implements IInvokedMethodListener {
  private Map<String, Boolean> resultTracker = new ConcurrentHashMap<>();

  @Override
  public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {
    String key = testResult.getInstanceName() + "." + method.getTestMethod().getMethodName();
    resultTracker.putIfAbsent(key, Boolean.TRUE);
  }

  @Override
  public void afterInvocation(IInvokedMethod method, ITestResult testResult) {
    Tcms tcms =
        method.getTestMethod().getConstructorOrMethod().getMethod().getAnnotation(Tcms.class);
    // Only report those tests to TestRail wherein our annotation is found.
    if (tcms == null) {
      return;
    }
    if (method.getTestMethod().isDataDriven()) {
      // For data driven tests we need a different logic
      String key = testResult.getInstanceName() + "." + method.getTestMethod().getMethodName();
      if (method.getTestMethod().hasMoreInvocation()) {
        Boolean result = resultTracker.get(key);
        result = result && (testResult.getStatus() == ITestResult.SUCCESS);
        resultTracker.put(key, result);
        return;
      }
      postResultsToTestRail(tcms, resultTracker.get(key));
    } else {
      postResultsToTestRail(tcms, testResult.getStatus() == ITestResult.SUCCESS);
    }
  }

  private void postResultsToTestRail(Tcms tcms, boolean pass) {
    String testCaseId = tcms.id();
    // Write logic here that takes care of posting results to the TCMS system
    System.err.println("Test case Id [" + testCaseId + "] passed ? " + pass);
  }
}

示例测试用例:

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

@Listeners(TestRailReporter.class)
public class SampleTestCase {

  @Test
  @Tcms(id = "TESTRAIL-1")
  public void testMethod() {
    Assert.assertTrue(true);
  }

  @Test(dataProvider = "dp")
  @Tcms(id = "TESTRAIL-2")
  public void dataDrivenTestWithSomeFailures(int i) {
    if (i % 2 == 0) {
      Assert.fail("simulating a failure");
    }
  }

  @Test(dataProvider = "dp")
  @Tcms(id = "TESTRAIL-3")
  public void dataDrivenTestWithNoFailures(int i) {
    Assert.assertTrue(i >= 0);
  }

  @DataProvider(name = "dp")
  public Object[][] getData() {
    return new Object[][] {{1}, {2}, {3}};
  }
}

输出:

Test case Id [TESTRAIL-3] passed ? true
Test case Id [TESTRAIL-2] passed ? false


java.lang.AssertionError: simulating a failure

    at org.testng.Assert.fail(Assert.java:97)
    at com.rationaleemotions.Whosebug.qn54224337.SampleTestCase.dataDrivenTestWithSomeFailures(SampleTestCase.java:21)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:131)
    at org.testng.internal.TestInvoker.invokeMethod(TestInvoker.java:570)
    at org.testng.internal.TestInvoker.invokeTestMethod(TestInvoker.java:170)
    at org.testng.internal.MethodRunner.runInSequence(MethodRunner.java:46)
    at org.testng.internal.TestInvoker$MethodInvocationAgent.invoke(TestInvoker.java:790)
    at org.testng.internal.TestInvoker.invokeTestMethods(TestInvoker.java:143)
    at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:146)
    at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:128)
    at org.testng.TestRunner.privateRun(TestRunner.java:763)
    at org.testng.TestRunner.run(TestRunner.java:594)
    at org.testng.SuiteRunner.runTest(SuiteRunner.java:398)
    at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:392)
    at org.testng.SuiteRunner.privateRun(SuiteRunner.java:355)
    at org.testng.SuiteRunner.run(SuiteRunner.java:304)
    at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:53)
    at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:96)
    at org.testng.TestNG.runSuitesSequentially(TestNG.java:1146)
    at org.testng.TestNG.runSuitesLocally(TestNG.java:1067)
    at org.testng.TestNG.runSuites(TestNG.java:997)
    at org.testng.TestNG.run(TestNG.java:965)
    at org.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:73)
    at org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:123)

Test case Id [TESTRAIL-1] passed ? true

===============================================
Default Suite
Total tests run: 7, Passes: 6, Failures: 1, Skips: 0
===============================================

编辑: 根据 OP 的评论,这是另一种方法。

方法二

正在使用的注释:

import static java.lang.annotation.ElementType.METHOD;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({METHOD})
public @interface Tcms {
  String id() default "";
}

对于数据驱动测试,数据提供者将传递给测试方法的参数如下所示:

import java.lang.annotation.Annotation;

public class TestData implements Tcms {

  private String tcmsId;
  private String data;

  public TestData(String tcmsId, String data) {
    this.tcmsId = tcmsId;
    this.data = data;
  }

  @Override
  public String id() {
    return tcmsId;
  }

  public String getData() {
    return data;
  }

  @Override
  public Class<? extends Annotation> annotationType() {
    return Tcms.class;
  }

  @Override
  public String toString() {
    return getData();
  }
}

侦听器如下所示:

import org.testng.IInvokedMethod;
import org.testng.IInvokedMethodListener;
import org.testng.ITestResult;

public class TestRailReporter2 implements IInvokedMethodListener {

  @Override
  public void afterInvocation(IInvokedMethod method, ITestResult testResult) {
    if (method.getTestMethod().isDataDriven()) {
      //Data driven tests need to be handled differently
      Object[] parameters = testResult.getParameters();
      if (parameters.length != 1) {
        //If theres more than one parameter, then dont do anything.
        return;
      }
      Object parameter = parameters[0];
      if (!(parameter instanceof Tcms)) {
        //If the parameter doesnt implement our interface dont do anything
        return;
      }
      postResultsToTestRail(
          (Tcms) parameter, testResult.getStatus() == ITestResult.SUCCESS, parameter.toString());
    } else {
      Tcms tcms =
          method.getTestMethod().getConstructorOrMethod().getMethod().getAnnotation(Tcms.class);
      if (tcms == null) {
        return;
      }
      postResultsToTestRail(tcms, testResult.getStatus() == ITestResult.SUCCESS);
    }
  }

  private void postResultsToTestRail(Tcms tcms, boolean pass) {
    String testCaseId = tcms.id();
    // Write logic here that takes care of posting results to the TCMS system
    System.err.println("Test case Id [" + testCaseId + "] passed ? " + pass);
  }

  private void postResultsToTestRail(Tcms tcms, boolean pass, String param) {
    String id = tcms.id();
    // Write logic here that takes care of posting results to the TCMS system
    System.err.println("Test case Id [" + id + "] with parameter [" + param + "] passed ? " + pass);
  }
}

测试 class 如下所示:

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

@Listeners(TestRailReporter2.class)
public class AnotherSampleTestCase {

  @Test
  @Tcms(id = "TESTRAIL-1")
  public void simpleTestMethod() {
    Assert.assertTrue(true);
  }

  @Test(dataProvider = "dp")
  public void dataDrivenTestMethod(TestData data) {
    Assert.assertFalse(data.getData().trim().isEmpty());
  }

  @DataProvider(name = "dp")
  public Object[][] getData() {
    return new Object[][] {
      {new TestData("TESTRAIL-2", "Jack")},
      {new TestData("TESTRAIL-3", "")},
      {new TestData("TESTRAIL-4", "Daniels")}
    };
  }
}

这是执行输出:

Test case Id [TESTRAIL-2] with parameter [Jack] passed ? true
Test case Id [TESTRAIL-3] with parameter [] passed ? false

java.lang.AssertionError: did not expect to find [false] but found [true]

    at org.testng.Assert.fail(Assert.java:97)
    at org.testng.Assert.failNotEquals(Assert.java:969)
    at org.testng.Assert.assertFalse(Assert.java:65)
    at org.testng.Assert.assertFalse(Assert.java:75)
    at com.rationaleemotions.Whosebug.qn54224337.AnotherSampleTestCase.dataDrivenTestMethod(AnotherSampleTestCase.java:19)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:131)
    at org.testng.internal.TestInvoker.invokeMethod(TestInvoker.java:570)
    at org.testng.internal.TestInvoker.invokeTestMethod(TestInvoker.java:170)
    at org.testng.internal.MethodRunner.runInSequence(MethodRunner.java:46)
    at org.testng.internal.TestInvoker$MethodInvocationAgent.invoke(TestInvoker.java:790)
    at org.testng.internal.TestInvoker.invokeTestMethods(TestInvoker.java:143)
    at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:146)
    at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:128)
    at org.testng.TestRunner.privateRun(TestRunner.java:763)
    at org.testng.TestRunner.run(TestRunner.java:594)
    at org.testng.SuiteRunner.runTest(SuiteRunner.java:398)
    at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:392)
    at org.testng.SuiteRunner.privateRun(SuiteRunner.java:355)
    at org.testng.SuiteRunner.run(SuiteRunner.java:304)
    at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:53)
    at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:96)
    at org.testng.TestNG.runSuitesSequentially(TestNG.java:1146)
    at org.testng.TestNG.runSuitesLocally(TestNG.java:1067)
    at org.testng.TestNG.runSuites(TestNG.java:997)
    at org.testng.TestNG.run(TestNG.java:965)
    at org.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:73)
    at org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:123)

Test case Id [TESTRAIL-4] with parameter [Daniels] passed ? true
Test case Id [TESTRAIL-1] passed ? true

===============================================
Default Suite
Total tests run: 4, Passes: 3, Failures: 1, Skips: 0
===============================================


Process finished with exit code 0