插件运行
一般我们使用IDEA执行测试都是通过如下图的方式
- 直接点击第5行绿色按钮运行整个测试类,如果有多个测试 方法则都会运行
- 直接点击第8行绿色按钮运行某个具体方法
这一点我们也可以通过IDEA的Run/Debug Configurations可以看到区别,如果我们通过第一种方式运行,Configuration下的TestKind为Class,如果通过第二种方式运行,Configuration下的TestKind为Method
插件调用
上节内容我们知道,TestNG框架的核心类就是org.testng.TestNG这个类,无论如何TestNG最终会调用到run方法执行测试,那插件启动也会调用到么,我们可以通过Debug方式去看下调用栈信息
如前面代码我们在第9行打个断点,以Debug方式运行,截图如下
通过截图我们可以很清楚的看到,最终调用TestNG类的run方法,不过在执行run方法之前,有两个类的方法要运行,如下
- com.intellij.rt.testng.RemoteTestNGStarter#main
- com.intellij.rt.testng.IDEARemoteTestNG#run
通过两个类的全限类名我们应该可以猜到这两个类是IDEA插件中的类,我们通过IDEA是无法查看这两个类的。还记得第一节中提到的idea插件源码吗?在 git hub testng_rt 中可以找到这两个类,下面我们就简单分析下这两个类吧,看下有些什么内容。
这里要说明的问题就是,由于IDEA使用插件代码版本和下载下来代码并不完全不一致,不过大致内容不一样,如果大家看到Debug代码行数与源码有出入,请忽略
RemoteTestNGStarter
在介绍RemoteTestNGStarter类之前,我们还是先把Debug下,这个类对应的一些变量粘出来,如下图
RemoteTestNGStarter 整个类只有一个main方法,直接进入主题 看代码中注释,大概步骤如下
- 解析main方法args参数,把参数赋值给resultArgs
- 解析临时文件中内容,找到 testng.xml位置
- 通过JCommander解析参数并赋值给IDEARemoteTestNG对象
- 执行IDEARemoteTestNG#run方法
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.rt.testng;
import com.beust.jcommander.JCommander;
import com.intellij.rt.execution.testFrameworks.ForkedDebuggerHelper;
import org.testng.CommandLineArgs;
import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RemoteTestNGStarter {
private static final String SOCKET = "-socket";
public static void main(String[] args) throws Exception {
int i = 0;
//参数 @name开头
String param = null;
//命令行文件 @@@开头
String commandFileName = null;
//工作目录 @w@开头
String workingDirs = null;
//最终收集的命令行参数,保证顺序使用了ArrayList
List<String> resultArgs = new ArrayList<String>();
//循环遍历传入的参数
for (; i < args.length; i++) {
String arg = args[i];
if (arg.startsWith("@name")) {
param = arg.substring(5);
continue;
} else if (arg.startsWith("@w@")) {
//获取工作目录,通过debug参数我们这个是有值的
workingDirs = arg.substring(3);
continue;
} else if (arg.startsWith("@@@")) {
commandFileName = arg.substring(3);
continue;
} else if (arg.startsWith(ForkedDebuggerHelper.DEBUG_SOCKET)) {
//String DEBUG_SOCKET = "-debugSocket";
continue;
} else if (arg.startsWith(SOCKET)) {
//获取端口号
final int port = Integer.parseInt(arg.substring(SOCKET.length()));
try {
final Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), port); //start collecting tests
final DataInputStream os = new DataInputStream(socket.getInputStream());
try {
//阻塞式等待,等待准备完毕的标识,这里应该有另一个线程通过此端口写数据
os.readBoolean();//wait for ready flag
} finally {
os.close();
}
} catch (IOException e) {
e.printStackTrace();
}
continue; //do not add socket to actual params
} else if (arg.equals("-temp")) {
//碰到此标识结束 for循环结束时,我们代码运行到此处,args只有前两个参数写到了resultArgs中 即 -usedefaultlisteners false
break;
}
resultArgs.add(arg);
}
//通过args最后一个参数获取文件位置
final File temp = new File(args[++i]);
final BufferedReader reader = new BufferedReader(new FileReader(temp));
final List<String> newArgs = new ArrayList<String>();
try {
final String cantRunMessage = "CantRunException";
while (true) {
//读取文件 我们可以通过上面的路径 直接在浏览器中查看,参考下方图片
String line = reader.readLine();
while (line == null) {
line = reader.readLine();
}
//line=C:\Users\AI\.IntelliJIdea2019.3\system\temp-testng-customsuite.xml
//逻辑不会走入这里
if (line.startsWith(cantRunMessage) && !new File(line).exists()) {
System.err.println(line.substring(cantRunMessage.length()));
while (true) {
line = reader.readLine();
if (line == null || line.equals("end")) break;
System.err.println(line);
}
System.exit(1);
return;
}
if (line.equals("end")) break;
newArgs.add(line);
}
} finally {
reader.close();
}
//此时把newArgs所有元素加入到resultArgs中,resultArgs中应该有3个元素 -usedefaultlisteners false C:\Users\AI\.IntelliJIdea2019.3\system\temp-testng-customsuite.xml
resultArgs.addAll(newArgs);
//逻辑不会进入
if (commandFileName != null) {
if (workingDirs != null && new File(workingDirs).length() > 0) {
System.exit(new TestNGForkedSplitter(workingDirs, newArgs)
.startSplitting(args, param, commandFileName, null));
return;
}
}
//调用TestNG 不过这里需要注意的是IDEARemoteTestNG 并不是TestNG框架中的类
/**
* 仔细看下面这些代码,和我们之前分析TestNG#main方法中逻辑很类似
* 这里也是先解析参数,赋值,调用run方法运行
*/
final IDEARemoteTestNG testNG = new IDEARemoteTestNG(param);
//这个对象熟悉吧,接收命令行参数的
CommandLineArgs cla = new CommandLineArgs();
/**
* 这里我们需要注意就是resultArgs只有三个参数,根据JCommander框架原则 useDefaultListeners属性默认为true,会被修改为false
* C:\Users\AI\.IntelliJIdea2019.3\system\temp-testng-customsuite.xml这个参数没有对应的命令行参数收集,最终会被收集到suiteFiles属性中
*/
new JCommander(Collections.singletonList(cla), resultArgs.toArray(new String[0]));
//赋值
testNG.configure(cla);
//运行
testNG.run();
}
}
IDEARemoteTestNG
从IDEARemoteTestNG源码中我们了解到, IDEARemoteTestNG继承TestNG类 并重写run方法
源码中configure方法也被重写,但是内部是直接调用,所以并不是真正的重写
因为configure首先被调用操作赋值,这里我们先从configure方法说起,这里我们只关注 useDefaultListeners属性与suiteFiles属性相关的操作
protected void configure(CommandLineArgs cla) {
...省略部分代码
// Note: can't use a Boolean field here because we are allowing a boolean
// parameter with an arity of 1 ("-usedefaultlisteners false")
//这里根据传入的值选择是否要使用默认监听器,这里的值为false
if (cla.useDefaultListeners != null) {
setUseDefaultListeners("true".equalsIgnoreCase(cla.useDefaultListeners));
}
...省略部分代码
//suiteFiles中是框架给我们生成的testng.xml路径
if (cla.suiteFiles != null) {
setTestSuites(cla.suiteFiles);
}
...省略部分代码
}
//把suites赋值给 m_stringSuites 属性
public void setTestSuites(List<String> suites) {
m_stringSuites = suites;
}
再看 run方法 大概逻辑如下
- 通过xml文件解析为XmlSuite对象
- 改变运行时的一些参数
- 加入监听器
- 运行
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.rt.testng;
import org.testng.CommandLineArgs;
import org.testng.TestNG;
import org.testng.collections.Lists;
import org.testng.xml.XmlClass;
import org.testng.xml.XmlInclude;
import org.testng.xml.XmlSuite;
import org.testng.xml.XmlTest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class IDEARemoteTestNG extends TestNG {
private final String myParam;
//有参构造函数
public IDEARemoteTestNG(String param) {
//param从上面传过来为null 此参数会被赋值给org.testng.xml.XmlInclude#m_invocationNumbers属性
myParam = param;
}
//递归把suites内容全部放入到 outSuites 对象中
private static void calculateAllSuites(List<XmlSuite> suites, List<XmlSuite> outSuites) {
for (XmlSuite s : suites) {
//先加入 父XmlSuite
outSuites.add(s);
//再去加入 子XmlSuite
calculateAllSuites(s.getChildSuites(), outSuites);
}
}
@Override
public void configure(CommandLineArgs cla) {
super.configure(cla);
}
@Override
public void run() {
try {
/**
* 调用 initializeSuitesAndJarFile方法 这个方法后面我们会详细讲解 这里只说出大概逻辑
* 方法内部就是根据 m_stringSuites属性去解析一个xml文件 生成 XmlSuite 对象
* 最终把 生成对象放入到 List<XmlSuite> m_suites的属性中
*/
initializeSuitesAndJarFile();
List<XmlSuite> suites = Lists.newArrayList();
calculateAllSuites(m_suites, suites);
if (suites.size() > 0) {
for (XmlSuite suite : suites) {
final List<XmlTest> tests = suite.getTests();
for (XmlTest test : tests) {
try {
//myParam为空 进不了此逻辑 可以暂时不关心内部逻辑
if (myParam != null) {
for (XmlClass aClass : test.getXmlClasses()) {
List<XmlInclude> includes = new ArrayList<XmlInclude>();
for (XmlInclude include : aClass.getIncludedMethods()) {
//myParam对应m_invocationNumbers 即失败调用次数
includes.add(new XmlInclude(include.getName(), Collections.singletonList(Integer.parseInt(myParam)), 0));
}
aClass.setIncludedMethods(includes);
}
}
} catch (NumberFormatException e) {
System.err.println("Invocation number: expected integer but found: " + myParam);
}
}
}
//插件监听器
attachListeners(new IDEATestNGRemoteListener());
//调用run方法
super.run();
} else {
System.out.println("##teamcity[enteredTheMatrix]");
System.err.println("Nothing found to run");
}
System.exit(0);
} catch (Throwable cause) {
cause.printStackTrace(System.err);
System.exit(-1);
}
}
/**
* 监听器我们只讲到这里,后续会有专门的文章介绍监听器,后续完成后我们会具体分析IDEA监听器的作用
* 需要注意的就是,这里的监听器都有一个 IDEATestNGRemoteListener 的类
* 实际上IDEA只是在实现接口时,每个实现类内部都持有一个 IDEATestNGRemoteListener 的引用
* 方法具体实现都是通过IDEATestNGRemoteListener来完成
*/
private void attachListeners(IDEATestNGRemoteListener listener) {
//ISuiteListener
addListener((Object) new IDEATestNGSuiteListener(listener));
//TestListener
addListener((Object) new IDEATestNGTestListener(listener));
try {
Class<?> configClass = Class.forName("com.intellij.rt.testng.IDEATestNGConfigurationListener");
Object configurationListener = configClass.getConstructor(new Class[]{IDEATestNGRemoteListener.class}).newInstance(listener);
//IConfigurationListener
addListener(configurationListener);
Class<?> invokeClass = Class.forName("com.intellij.rt.testng.IDEATestNGInvokedMethodListener");
Object invokedMethodListener = invokeClass.getConstructor(new Class[]{IDEATestNGRemoteListener.class}).newInstance(listener);
//IInvokedMethodListener
addListener(invokedMethodListener);
//start with configuration started if invoke method listener was not added, otherwise with
//反射调用IDEATestNGConfigurationListener#setIgnoreStarted方法
configClass.getMethod("setIgnoreStarted").invoke(configurationListener);
} catch (Throwable ignored) {
}
}
}