写点什么

《TestNG》源码学习笔记

用户头像
吴大山
关注
发布于: 2021 年 02 月 20 日

原文链接:http://wudashan.com/2020/09/13/testng-learning/


框架介绍


英文原版


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);      }    }  }  // 问题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#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);      }    }  }	}
复制代码


参考链接



用户头像

吴大山

关注

还未添加个人签名 2018.05.04 加入

还未添加个人简介

评论

发布
暂无评论
《TestNG》源码学习笔记