美文网首页TestNG与JUnit
TestNG框架源码走读一:入口

TestNG框架源码走读一:入口

作者: 测试你个头 | 来源:发表于2018-02-13 16:03 被阅读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,研究下它如何实现执行测试套/测试用例。

    相关文章

      网友评论

        本文标题:TestNG框架源码走读一:入口

        本文链接:https://www.haomeiwen.com/subject/uqmdwxtx.html