《TestNG》源码学习笔记

1,080 阅读5分钟

原文链接:wudashan.com/2020/09/13/…

框架介绍

英文原版

TestNG is a testing framework inspired from JUnit and NUnit but introducing some new functionalities that make it more powerful and easier to use, such as: * Annotations.* Run your tests in arbitrarily big thread pools with various policies available (all methods in their own thread, one thread per test class, etc...).* Test that your code is multithread safe.* Flexible test configuration.* Support for data-driven testing (with @DataProvider).* Support for parameters.* Powerful execution model (no more TestSuite).* Supported by a variety of tools and plug-ins (Eclipse, IDEA, Maven, etc...).* Embeds BeanShell for further flexibility.* Default JDK functions for runtime and logging (no dependencies).* Dependent methods for application server testing. TestNG is designed to cover all categories of tests:  unit, functional, end-to-end, integration, etc...

中文翻译

TestNG是一个受JUnit和NUnit启发的测试框架,但引入了一些使其更强大且更易于使用的新功能,例如:

  • 注解。
  • 线程池中运行测试用例。
  • 支持测试代码是否多线程安全。
  • 灵活的测试配置。
  • 支持数据驱动的测试(使用@DataProvider)。
  • 以插件形式被各种工具(Eclipse,IDEA,Maven等)集成。

TestNG旨在涵盖所有类别的测试:单元,功能,端到端,集成等。

源码版本

<dependency>    <groupId>org.testng</groupId>    <artifactId>testng</artifactId>    <version>6.8</version>    <scope>test</scope></dependency>

带着问题去学习

通过TestNG框架的官方介绍,我们知道了它主要提供了哪些功能,对应的我们需要通过几个问题去理解其如何实现(原理)?

注解功能

TestNG如何发现需要被测试的方法?

通过Java两大特性,注解+反射,找到被测方法。具体原理为先通过main函数入参或testng.xml配置文件获取需要扫描的类,再通过反射获取类信息,判断是否有@Test注解,如果有则表示该类的方法需要测试。

TestNG如何支持用户感知框架运行时状态?

通过开放各种Listener接口(父类为org.testng.ITestNGListener),如IExecutionListener、IConfigurationListener、IInvokedMethodListener等,并在运行时进行回调,使用户感知当前运行状态。

线程池中运行测试用例功能

TestNG如何支持多线程执行测试用例?

通过Java内置的ThreadPoolExecutor线程池实现多线程执行测试用例。并且支持suite/tests/classes/methods/instances五种维度的多线程场景:suite多线程实现在org.testng.TestNG#runSuitesLocally,tests多线程实现在org.testng.SuiteRunner#runInParallelTestMode,classes/methods/instances多线程实现都在org.testng.TestRunner#privateRun,后三者区别在于通过org.testng.TestRunner#createWorkers创建的Work数量不同:classes场景该类的所有被测方法都在一个Work里串行执行,methods场景每个被测方法自己单独一个Work,instances场景每个被测方法实例单独一个Work。

灵活的测试配置功能

TestNG如何解析命令行参数?

使用JCommander第三方框架,解析main入口函数里用户通过命令行传入的args参数,并转成CommandLineArgs对象。

TestNG如何解析testng.xml配置文件?

实现了一个Parser文件处理器,支持对xml和yaml格式的配置文件进行解析,通过类继承关系可以知道TestNG支持通过SAX和DOM两种方式解析xml文件。

支持数据驱动的测试功能

TestNG如何支持参数化执行用例?

通过找到@DataProvider注解的方法,执行该方法并返回List<Object[]>对象(外层List代表被测方法要执行的次数,内层Object[]代表每次执行被测方法时传入的形参),或testng.xml里的<parameter>参数,得到参数列表,并在反射调用用例时传入参数。

其他功能

TestNG如何展示用例执行结果?

定义了IReporter接口,在用例执行结束后,回调其generateReport方法,并将整个用例结果SuiteResult传给该方法。

TestNG如何解决测试用例之间的依赖顺序?

通过这个数据结构,将A方法依赖B方法,转义成A->B的单向图,实现用例之间存在依赖时,调用的先后顺序。

执行时序图

TestNG.main()

TestNG.runSuitesLocally()

TestRunner.createWorkersAndRun()

关键类类图

TestNG 主程序

Parser 文件解析器

XmlSuite 测试数据

Listener 监听器

IAnnotation 注解接口

SuiteRunner 执行类

ThreadPoolExecutor 线程池

DynamicGraph 图数据结构

IReporter 执行结果

经典代码

反射获取Class类

// org.testng.internal.ClassHelper#forNamepublic static Class<?> forName(final String className) {  // 获取类加载器集合  Vector<ClassLoader> allClassLoaders = new Vector<ClassLoader>();  ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();  if (contextClassLoader != null) {    allClassLoaders.add(contextClassLoader);  }  if (m_classLoaders != null) {    allClassLoaders.addAll(m_classLoaders);  }    // 遍历类加载器,看谁能加载成功类  int count = 0;  for (ClassLoader classLoader : allClassLoaders) {    ++count;    if (null == classLoader) {      continue;    }    try {      return classLoader.loadClass(className);    }    catch(ClassNotFoundException ex) {      // With additional class loaders, it is legitimate to ignore ClassNotFoundException      if (null == m_classLoaders || m_classLoaders.size() == 0) {        logClassNotFoundError(className, ex);      }    }  }  // 问题1Class.forName() 和 ClassLoader.loadClass() 有什么不同?  // 答案:https://stackoverflow.com/questions/8100376/class-forname-vs-classloader-loadclass-which-to-use-for-dynamic-loading   // 问题2Class.forName() 使用哪个类加器进行加载?  // 答案:默认会使用调用类的类加载器来进行类加载,顺便理解双亲委派机制,(双亲是哪双亲?)。  try {    return Class.forName(className);  }  catch(ClassNotFoundException cnfe) {    logClassNotFoundError(className, cnfe);    return null;  }}

Java SPI获取Listener实现类

// org.testng.TestNG#addServiceLoaderListenersprivate void addServiceLoaderListeners() {  Iterable<ITestNGListener> loader;  try {    if (m_serviceLoaderClassLoader != null) {      // spi原理:加载META-INF/services/路径下的文件      // 文件名是接口,文件内容每行是实现类,反射创建实现类实例,并强转成接口      // 使用到了懒加载机制      loader = ServiceLoader.load(ITestNGListener.class, m_serviceLoaderClassLoader);    } else {      loader = ServiceLoader.load(ITestNGListener.class);    }    for (ITestNGListener l : loader) {      addListener(l);      addServiceLoaderListener(l);    }  } catch (Exception ex) {      // Ignore  }}

图数据结构找无依赖的方法

// 图由节点和边组成public class DynamicGraph<T> {	  // Set记录节点,这里区分节点3个状态,因为已完成的节点可认为不再依赖  private Set<T> m_nodesReady = Sets.newLinkedHashSet();  private Set<T> m_nodesRunning = Sets.newLinkedHashSet();  private Set<T> m_nodesFinished = Sets.newLinkedHashSet();  // Map记录边  private ListMultiMap<T, T> m_dependedUpon = Maps.newListMultiMap();  private ListMultiMap<T, T> m_dependingOn = Maps.newListMultiMap();	  // 往图中增加节点  public void addNode(T node) {    m_nodesReady.add(node);  }	  // 往图中增加边  public void addEdge(T from, T to) {    addNode(from);    addNode(to);    m_dependingOn.put(to, from);    m_dependedUpon.put(from, to);  }	  // 获取没有依赖的节点  public List<T> getFreeNodes() {    List<T> result = Lists.newArrayList();    for (T m : m_nodesReady) {      // 一个节点如何是“自由的”,那应该它没有依赖任何节点,或者依赖的节点状态都是已完成      List<T> du = m_dependedUpon.get(m);      if (!m_dependedUpon.containsKey(m)) {        result.add(m);      } else if (getUnfinishedNodes(du).size() == 0) {        result.add(m);      }    }    return result;  }    // 获取未到达终态的节点列表  private Collection<? extends T> getUnfinishedNodes(List<T> nodes) {    Set<T> result = Sets.newHashSet();    for (T node : nodes) {      if (m_nodesReady.contains(node) || m_nodesRunning.contains(node)) {        result.add(node);      }    }    return result;  }    // 设置节点状态  public void setStatus(T node, Status status) {    // 先将节点从原集合Set中删除    removeNode(node);    // 再插入到对应状态的新集合里    switch(status) {      case READY: m_nodesReady.add(node); break;      case RUNNING: m_nodesRunning.add(node); break;      case FINISHED: m_nodesFinished.add(node); break;      default: throw new IllegalArgumentException();    }  }   // 删除节点  private void removeNode(T node) {    // 这种代码有点难理解,就是三个集合依次删除,成功就返回    if (!m_nodesReady.remove(node)) {      if (!m_nodesRunning.remove(node)) {        m_nodesFinished.remove(node);      }    }  }	}

参考链接