TestNG与JUnit

TestNG框架源码走读一:入口

2018-02-13  本文已影响107人  测试你个头

如果仅仅是想知道TestNG如何使用,可以参考官方文档

我们运行TestNG从开始执行用例到最终输出报告,是通过一条命令行实现的:

$java org.testng.TestNG testng.xml

这段命令行的背后代码是如何运行的?

走读TestNg源码的目的一是学习优秀的框架是如何开发的,二是了解框架的一些配置、特性、用法背后的具体实现,可以帮忙我们做一些二次开发。

1.从github/testng下载源码并导入IDEA

TestNG使用gradle作为构建工具,可以学习下gradle如何进行java程序编译和打包就可以编译TestNG的源码了


使用gradle编译TestNG源码 编译生成testng.jar

2.查找TestNG源码入口:

注释:命令行执行TestNG($java org.testng.TestNG testng1.xml [testng2.xml testng3.xml ...])的入口

  /**
   * The TestNG entry point for command line execution.
   *
   * @param argv the TestNG command line parameters.
   * @throws FileNotFoundException
   */
  public static void main(String[] argv) {
    TestNG testng = privateMain(argv, null);
    System.exit(testng.getStatus());
  }

3.privateMain去除一些非核心代码后如下

  public static TestNG privateMain(String[] argv, ITestListener listener) {
    // 1.实例TestNG对象
    TestNG result = new TestNG();
    // 2.添加listenner
    result.addListener((Object)listener);
    // 3.解析参数并配置TestNG对象result
    CommandLineArgs cla = new CommandLineArgs();
    m_jCommander = new JCommander(cla, argv);
    validateCommandLineParameters(cla);
    result.configure(cla);
    // 4.执行用例
    result.run();
    // 5.返回结果
    return result;
  }

5.看看执行用例的时序图,result#run()最终是调用了TestNG#runSuiteLocally()来实现核心逻辑:


TestNG#runSuiteLocally()的实现如下(去除非核心代码)

  public List<ISuite> runSuitesLocally() {
    SuiteRunnerMap suiteRunnerMap = new SuiteRunnerMap();
    // 判断是否有测试用例,没有报错No test suite found. Nothing to run
    if (m_suites.size() > 0) {
      // 重要:创建测试套执行器
      for (XmlSuite xmlSuite : m_suites) {
        createSuiteRunners(suiteRunnerMap, xmlSuite);
      }

      // 重要:执行测试套
      if (m_suiteThreadPoolSize == 1 && !m_randomizeSuites) {
        // 串行执行测试套
        for (XmlSuite xmlSuite : m_suites) {
          // 核心逻辑1:递归执行测试套(先执行子测试套,然后再执行父测试套)
          runSuitesSequentially(xmlSuite, suiteRunnerMap, getVerbose(xmlSuite),
              getDefaultSuiteName());
        }
      } else {
        // 多线程执行测试套
        DynamicGraph<ISuite> suiteGraph = new DynamicGraph<>();
        for (XmlSuite xmlSuite : m_suites) {
          populateSuiteGraph(suiteGraph, suiteRunnerMap, xmlSuite);
        }

        IThreadWorkerFactory<ISuite> factory = new SuiteWorkerFactory(suiteRunnerMap,
          0 /* verbose hasn't been set yet */, getDefaultSuiteName());
        GraphThreadPoolExecutor<ISuite> pooledExecutor =
                new GraphThreadPoolExecutor<>("suites", suiteGraph, factory, m_suiteThreadPoolSize,
                        m_suiteThreadPoolSize, Integer.MAX_VALUE, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>());

        Utils.log("TestNG", 2, "Starting executor for all suites");
        
        // 核心逻辑2:并发执行测试套
        pooledExecutor.run();

        // 等待测试套执行结束
        try {
          pooledExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
          pooledExecutor.shutdownNow();
        }
        catch (InterruptedException handled) {
          Thread.currentThread().interrupt();
          error("Error waiting for concurrent executors to finish " + handled.getMessage());
        }
      }
    }
    else {
      setStatus(HAS_NO_TEST);
      error("No test suite found. Nothing to run");
      usage();
    }

    return Lists.newArrayList(suiteRunnerMap.values());
  }

runSuitesLocally有3个核心逻辑需要详细走读下代码(放在后续SuiteRunner代码走读里研究):
1.createSuiteRunners创建测试套执行器的实现。
2.runSuiteSequentially串行执行测试套的实现。
3.populateSuiteGraph/GraphThreadPoolExecutor等批量并行执行测试套的实现。

同时需要看下m_suites变量是如何初始化的,m_suites变量是读取自testng.xml中的suite节点

<suite name="xspace-oms-payment" verbose="1">
    <test name="payment">
        <packages>
            <package name="com.xcloud.oms.payment">
            </package>
        </packages>
    </test>
</suite>

6.m_suites的初始化:
回到TestNG#privateMain()中调用的result.run()方法:

  /**
   * Run TestNG.
   */
  public void run() {
    initializeEverything();
    ......
    List<ISuite> suiteRunners = runSuites();
    ......
  }

initializeEverything()的实现如下,整体逻辑是:从命令行参数->jar包路径->jar包中找到配置文件并解析出测试套。

  public void initializeEverything() {
    // The Eclipse plug-in (RemoteTestNG) might have invoked this method already
    // so don't initialize suites twice.
    if (m_isInitialized) {
      return;
    }

    initializeSuitesAndJarFile();
    initializeConfiguration();
    initializeDefaultListeners();
    initializeCommandLineSuites();
    initializeCommandLineSuitesParams();
    initializeCommandLineSuitesGroups();

    m_isInitialized = true;
  }

这里调用了initializeSuitesAndJarFile()实现了m_suites的初始化,去除和m_suites初始化无关的代码后:

public void initializeSuitesAndJarFile() {
    if (m_suites.size() > 0) {
        //to parse the suite files (<suite-file>), if any
        for (XmlSuite s: m_suites) {
            for (String suiteFile : s.getSuiteFiles()) {
                try {
                    Collection<XmlSuite> childSuites;
                    if (s.getFileName() != null) {
                      Path rootPath = Paths.get(s.getFileName()).getParent();
                      try (InputStream is = Files.newInputStream(rootPath.resolve(suiteFile))) {
                        childSuites = getParser(is).parse();
                      }
                    } else {
                      childSuites = getParser(suiteFile).parse();
                    }
                    for (XmlSuite cSuite : childSuites){
                        cSuite.setParentSuite(s);
                        s.getChildSuites().add(cSuite);
                    }
                } catch (IOException e) {
                    e.printStackTrace(System.out);
                }
            }
        }
      return;
    }

    // m_stringSuites是在TestNG#privateMain()中调用result.configure()里进行初始化,是通过命令行传入的测试套配置xml文件路径
    for (String suitePath : m_stringSuites) {
      if(LOGGER.isDebugEnabled()) {
        LOGGER.debug("suiteXmlPath: \"" + suitePath + "\"");
      }
      try {
        // 从xml文件中解析测试套
        Collection<XmlSuite> allSuites = getParser(suitePath).parse();

        for (XmlSuite s : allSuites) {
          // 如果参数中指定了测试用例名称,只执行指定用例
          if (m_testNames != null) {
            m_suites.add(extractTestNames(s, m_testNames));
          }
          else {
            m_suites.add(s);
          }
        }
      }
      catch(IOException e) {
        e.printStackTrace(System.out);
      } catch(Exception ex) {
        // Probably a Yaml exception, unnest it
        Throwable t = ex;
        while (t.getCause() != null) t = t.getCause();
        if (t instanceof TestNGException) throw (TestNGException) t;
        else throw new TestNGException(t);
      }
    }

    // 如果测试套是通过命令行传入,优先级要高于在jar包路径下的测试套
    if (m_jarPath != null && m_stringSuites.size() > 0) {
      StringBuilder suites = new StringBuilder();
      for (String s : m_stringSuites) {
        suites.append(s);
      }
      Utils.log("TestNG", 2, "Ignoring the XML file inside " + m_jarPath + " and using "
          + suites + " instead");
      return;
    }
    if (isStringEmpty(m_jarPath)) {
      return;
    }

    // 没有指定xml文件,但是传入了一个jar包,试图从jar包中找到xml配置文件
    File jarFile = new File(m_jarPath);

    try {

      Utils.log("TestNG", 2, "Trying to open jar file:" + jarFile);

      boolean foundTestngXml = false;
      List<String> classes = Lists.newArrayList();
      try (JarFile jf = new JarFile(jarFile)) {
        Enumeration<JarEntry> entries = jf.entries();
        while (entries.hasMoreElements()) {
          JarEntry je = entries.nextElement();
          if (je.getName().equals(m_xmlPathInJar)) {
            Parser parser = getParser(jf.getInputStream(je));
            Collection<XmlSuite> suites = parser.parse();
            for (XmlSuite suite : suites) {
              // If test names were specified, only run these test names
              if (m_testNames != null) {
                m_suites.add(extractTestNames(suite, m_testNames));
              } else {
                m_suites.add(suite);
              }
            }

            foundTestngXml = true;
            break;
          } else if (je.getName().endsWith(".class")) {
            int n = je.getName().length() - ".class".length();
            classes.add(je.getName().replace("/", ".").substring(0, n));
          }
        }
      }
      if (! foundTestngXml) {
        Utils.log("TestNG", 1,
            "Couldn't find the " + m_xmlPathInJar + " in the jar file, running all the classes");
        XmlSuite xmlSuite = new XmlSuite();
        xmlSuite.setVerbose(0);
        xmlSuite.setName("Jar suite");
        XmlTest xmlTest = new XmlTest(xmlSuite);
        List<XmlClass> xmlClasses = Lists.newArrayList();
        for (String cls : classes) {
          XmlClass xmlClass = new XmlClass(cls);
          xmlClasses.add(xmlClass);
        }
        xmlTest.setXmlClasses(xmlClasses);
        m_suites.add(xmlSuite);
      }
    }
    catch(IOException ex) {
      ex.printStackTrace();
    }
  }

代码中用到的2个变量m_stringSuites和m_jarPath都是通过可选命令行参数传入,它们都是在TestNG#privateMain()中调用result.configure()里进行初始化,具体实现可以看下result.configure()的源码。

命令行参数格式如下:
$java org.testng.TestNG m_stringSuites -testjar m_jarPath

例如:
$java org.testng.TestNG testng.xml

对于TestNG的入口代码的走读就到此结束,接下来会走读TestNG的核心类SuiteRunner,研究下它如何实现执行测试套/测试用例。

上一篇下一篇

猜你喜欢

热点阅读