TestNG框架源码走读一:入口

如果仅仅想知道如何使用TestNG,请阅读官方文档。如果想知道TestNG背后代码是如何运行的,继续往下:

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

$java org.testng.TestNG testng.xml

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

1.首先从github/testng下载源码

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


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

2.查找TestNG源码入口main函数:

java org.testng.TestNG这条命令的意思是执行TestNGmain函数,所以我们首先找到TestNG这个类的main函数定义

执行$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.TestNG#privateMain核心逻辑如下

  public static TestNG privateMain(String[] argv, ITestListener listener) {
    TestNG result = new TestNG();
    result.addListener((Object)listener);

    // 1.解析参数并配置TestNG对象result
    CommandLineArgs cla = new CommandLineArgs();
    m_jCommander = new JCommander(cla, argv);
    validateCommandLineParameters(cla);
    result.configure(cla);

    // 2.执行用例
    result.run();

    return result;
  }

4.上一步中TestNG#run的代码如下:

/** Run TestNG. */
  public void run() {
    initializeEverything();
    sanityCheck();

    runExecutionListeners(true /* start */);

    runSuiteAlterationListeners();

    m_start = System.currentTimeMillis();

    // 执行用例
    List<ISuite> suiteRunners = runSuites();

    m_end = System.currentTimeMillis();

    // 生成用例报告
    if (null != suiteRunners) {
      generateReports(suiteRunners);
    }

    runExecutionListeners(false /* finish */);
    ......
  }

5.看看执行用例的时序图,TestNG#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,研究下它如何实现执行测试套/测试用例。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352