TestNG框架源码走读一:入口
如果仅仅是想知道TestNG如何使用,可以参考官方文档
我们运行TestNG从开始执行用例到最终输出报告,是通过一条命令行实现的:
$java org.testng.TestNG testng.xml
这段命令行的背后代码是如何运行的?
走读TestNg源码的目的一是学习优秀的框架是如何开发的,二是了解框架的一些配置、特性、用法背后的具体实现,可以帮忙我们做一些二次开发。
1.从github/testng下载源码并导入IDEA
TestNG使用gradle作为构建工具,可以学习下gradle如何进行java程序编译和打包就可以编译TestNG的源码了
使用gradle编译TestNG源码 编译生成testng.jar
2.查找TestNG源码入口:
注释:命令行执行TestNG($java org.testng.TestNG testng1.xml [testng2.xml testng3.xml ...]
)的入口
/**
* The TestNG entry point for command line execution.
*
* @param argv the TestNG command line parameters.
* @throws FileNotFoundException
*/
public static void main(String[] argv) {
TestNG testng = privateMain(argv, null);
System.exit(testng.getStatus());
}
3.privateMain去除一些非核心代码后如下
public static TestNG privateMain(String[] argv, ITestListener listener) {
// 1.实例TestNG对象
TestNG result = new TestNG();
// 2.添加listenner
result.addListener((Object)listener);
// 3.解析参数并配置TestNG对象result
CommandLineArgs cla = new CommandLineArgs();
m_jCommander = new JCommander(cla, argv);
validateCommandLineParameters(cla);
result.configure(cla);
// 4.执行用例
result.run();
// 5.返回结果
return result;
}
5.看看执行用例的时序图,result#run()最终是调用了TestNG#runSuiteLocally()来实现核心逻辑:
TestNG#runSuiteLocally()的实现如下(去除非核心代码)
public List<ISuite> runSuitesLocally() {
SuiteRunnerMap suiteRunnerMap = new SuiteRunnerMap();
// 判断是否有测试用例,没有报错No test suite found. Nothing to run
if (m_suites.size() > 0) {
// 重要:创建测试套执行器
for (XmlSuite xmlSuite : m_suites) {
createSuiteRunners(suiteRunnerMap, xmlSuite);
}
// 重要:执行测试套
if (m_suiteThreadPoolSize == 1 && !m_randomizeSuites) {
// 串行执行测试套
for (XmlSuite xmlSuite : m_suites) {
// 核心逻辑1:递归执行测试套(先执行子测试套,然后再执行父测试套)
runSuitesSequentially(xmlSuite, suiteRunnerMap, getVerbose(xmlSuite),
getDefaultSuiteName());
}
} else {
// 多线程执行测试套
DynamicGraph<ISuite> suiteGraph = new DynamicGraph<>();
for (XmlSuite xmlSuite : m_suites) {
populateSuiteGraph(suiteGraph, suiteRunnerMap, xmlSuite);
}
IThreadWorkerFactory<ISuite> factory = new SuiteWorkerFactory(suiteRunnerMap,
0 /* verbose hasn't been set yet */, getDefaultSuiteName());
GraphThreadPoolExecutor<ISuite> pooledExecutor =
new GraphThreadPoolExecutor<>("suites", suiteGraph, factory, m_suiteThreadPoolSize,
m_suiteThreadPoolSize, Integer.MAX_VALUE, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
Utils.log("TestNG", 2, "Starting executor for all suites");
// 核心逻辑2:并发执行测试套
pooledExecutor.run();
// 等待测试套执行结束
try {
pooledExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
pooledExecutor.shutdownNow();
}
catch (InterruptedException handled) {
Thread.currentThread().interrupt();
error("Error waiting for concurrent executors to finish " + handled.getMessage());
}
}
}
else {
setStatus(HAS_NO_TEST);
error("No test suite found. Nothing to run");
usage();
}
return Lists.newArrayList(suiteRunnerMap.values());
}
runSuitesLocally有3个核心逻辑需要详细走读下代码(放在后续SuiteRunner代码走读里研究):
1.createSuiteRunners创建测试套执行器的实现。
2.runSuiteSequentially串行执行测试套的实现。
3.populateSuiteGraph/GraphThreadPoolExecutor等批量并行执行测试套的实现。
同时需要看下m_suites变量是如何初始化的,m_suites变量是读取自testng.xml中的suite节点
<suite name="xspace-oms-payment" verbose="1">
<test name="payment">
<packages>
<package name="com.xcloud.oms.payment">
</package>
</packages>
</test>
</suite>
6.m_suites的初始化:
回到TestNG#privateMain()中调用的result.run()方法:
/**
* Run TestNG.
*/
public void run() {
initializeEverything();
......
List<ISuite> suiteRunners = runSuites();
......
}
initializeEverything()的实现如下,整体逻辑是:从命令行参数->jar包路径->jar包中找到配置文件并解析出测试套。
public void initializeEverything() {
// The Eclipse plug-in (RemoteTestNG) might have invoked this method already
// so don't initialize suites twice.
if (m_isInitialized) {
return;
}
initializeSuitesAndJarFile();
initializeConfiguration();
initializeDefaultListeners();
initializeCommandLineSuites();
initializeCommandLineSuitesParams();
initializeCommandLineSuitesGroups();
m_isInitialized = true;
}
这里调用了initializeSuitesAndJarFile()实现了m_suites的初始化,去除和m_suites初始化无关的代码后:
public void initializeSuitesAndJarFile() {
if (m_suites.size() > 0) {
//to parse the suite files (<suite-file>), if any
for (XmlSuite s: m_suites) {
for (String suiteFile : s.getSuiteFiles()) {
try {
Collection<XmlSuite> childSuites;
if (s.getFileName() != null) {
Path rootPath = Paths.get(s.getFileName()).getParent();
try (InputStream is = Files.newInputStream(rootPath.resolve(suiteFile))) {
childSuites = getParser(is).parse();
}
} else {
childSuites = getParser(suiteFile).parse();
}
for (XmlSuite cSuite : childSuites){
cSuite.setParentSuite(s);
s.getChildSuites().add(cSuite);
}
} catch (IOException e) {
e.printStackTrace(System.out);
}
}
}
return;
}
// m_stringSuites是在TestNG#privateMain()中调用result.configure()里进行初始化,是通过命令行传入的测试套配置xml文件路径
for (String suitePath : m_stringSuites) {
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("suiteXmlPath: \"" + suitePath + "\"");
}
try {
// 从xml文件中解析测试套
Collection<XmlSuite> allSuites = getParser(suitePath).parse();
for (XmlSuite s : allSuites) {
// 如果参数中指定了测试用例名称,只执行指定用例
if (m_testNames != null) {
m_suites.add(extractTestNames(s, m_testNames));
}
else {
m_suites.add(s);
}
}
}
catch(IOException e) {
e.printStackTrace(System.out);
} catch(Exception ex) {
// Probably a Yaml exception, unnest it
Throwable t = ex;
while (t.getCause() != null) t = t.getCause();
if (t instanceof TestNGException) throw (TestNGException) t;
else throw new TestNGException(t);
}
}
// 如果测试套是通过命令行传入,优先级要高于在jar包路径下的测试套
if (m_jarPath != null && m_stringSuites.size() > 0) {
StringBuilder suites = new StringBuilder();
for (String s : m_stringSuites) {
suites.append(s);
}
Utils.log("TestNG", 2, "Ignoring the XML file inside " + m_jarPath + " and using "
+ suites + " instead");
return;
}
if (isStringEmpty(m_jarPath)) {
return;
}
// 没有指定xml文件,但是传入了一个jar包,试图从jar包中找到xml配置文件
File jarFile = new File(m_jarPath);
try {
Utils.log("TestNG", 2, "Trying to open jar file:" + jarFile);
boolean foundTestngXml = false;
List<String> classes = Lists.newArrayList();
try (JarFile jf = new JarFile(jarFile)) {
Enumeration<JarEntry> entries = jf.entries();
while (entries.hasMoreElements()) {
JarEntry je = entries.nextElement();
if (je.getName().equals(m_xmlPathInJar)) {
Parser parser = getParser(jf.getInputStream(je));
Collection<XmlSuite> suites = parser.parse();
for (XmlSuite suite : suites) {
// If test names were specified, only run these test names
if (m_testNames != null) {
m_suites.add(extractTestNames(suite, m_testNames));
} else {
m_suites.add(suite);
}
}
foundTestngXml = true;
break;
} else if (je.getName().endsWith(".class")) {
int n = je.getName().length() - ".class".length();
classes.add(je.getName().replace("/", ".").substring(0, n));
}
}
}
if (! foundTestngXml) {
Utils.log("TestNG", 1,
"Couldn't find the " + m_xmlPathInJar + " in the jar file, running all the classes");
XmlSuite xmlSuite = new XmlSuite();
xmlSuite.setVerbose(0);
xmlSuite.setName("Jar suite");
XmlTest xmlTest = new XmlTest(xmlSuite);
List<XmlClass> xmlClasses = Lists.newArrayList();
for (String cls : classes) {
XmlClass xmlClass = new XmlClass(cls);
xmlClasses.add(xmlClass);
}
xmlTest.setXmlClasses(xmlClasses);
m_suites.add(xmlSuite);
}
}
catch(IOException ex) {
ex.printStackTrace();
}
}
代码中用到的2个变量m_stringSuites和m_jarPath都是通过可选命令行参数传入,它们都是在TestNG#privateMain()中调用result.configure()里进行初始化,具体实现可以看下result.configure()的源码。
命令行参数格式如下:
$java org.testng.TestNG m_stringSuites -testjar m_jarPath
例如:
$java org.testng.TestNG testng.xml
对于TestNG的入口代码的走读就到此结束,接下来会走读TestNG的核心类SuiteRunner,研究下它如何实现执行测试套/测试用例。