原文链接: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 启发的测试框架,但引入了一些使其更强大且更易于使用的新功能,例如:
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); } } } }
复制代码
参考链接
评论