《TestNG》学习笔记

Posted by Wudashan on September 13, 2020

框架介绍

英文原版

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#forName
public 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);
      }
    }
  }
  // 问题1:Class.forName() 和 ClassLoader.loadClass() 有什么不同?
  // 答案:https://stackoverflow.com/questions/8100376/class-forname-vs-classloader-loadclass-which-to-use-for-dynamic-loading

  // 问题2:Class.forName() 使用哪个类加器进行加载?
  // 答案:默认会使用调用类的类加载器来进行类加载,顺便理解双亲委派机制,(双亲是哪双亲?)。
  try {
    return Class.forName(className);
  }
  catch(ClassNotFoundException cnfe) {
    logClassNotFoundError(className, cnfe);
    return null;
  }
}

Java SPI获取Listener实现类

// org.testng.TestNG#addServiceLoaderListeners
private 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);
      }
    }
  }
	
}

参考链接