junit源码解析--初始化阶段

时间:2023-03-08 16:14:32



OK,我们接着上篇整理。上篇博客中已经列出的junit的几个核心的类,这里我们开始整理junit完整的生命周期。

  • JUnit 的完整生命周期分为 3 个阶段:初始化阶段、运行阶段和结果捕捉阶段。

这篇这里先来整理下junit的初始化阶段。也就是创建 Testcase 及 TestSuite。先来贴出junit测试框架入口:

/**
* @创建时间: 2016年1月21日
* @相关参数: @param args
* @功能描述: 测试框架入口
*/
public static void main(String[] args)
{
TestRunner aTestRunner = new TestRunner();
try
{
//String[] linkinArgs = new String[] { "-m", "org.linkinpark.commons.textui.LinkinTest.testLinkin4Normal" };
// String[] linkinArgs = new String[] { "-v", "-m",
// "org.linkinpark.commons.textui.LinkinTest.testLinkin4Normal" };
// String[] linkinArgs = new String[] { "-c", "org.linkinpark.commons.textui.LinkinTest" };
// String[] linkinArgs = new String[] { "org.linkinpark.commons.textui.LinkinTest" };
// String[] linkinArgs = new String[] { "-wait", "org.linkinpark.commons.textui.LinkinTest" };
String[] linkinArgs = new String[] { "org.linkinpark.commons.textui.LinkinTestAll" };
TestResult testResult = aTestRunner.start(linkinArgs);
if (!testResult.wasSuccessful())
{
System.exit(FAILURE_EXIT);
}
System.exit(SUCCESS_EXIT);
}
catch (Exception e)
{
System.err.println(e.getMessage());
System.exit(EXCEPTION_EXIT);
}
}

初始化阶段作一些重要的初始化工作,它的入口点在 junit.textui.TestRunner 的 main 方法。该方法首先创建一个 TestRunner 实例 aTestRunner。之后 main 函数中主体工作函数为 TestResult r = aTestRunner.start(args) 。这里贴出测试执行器runner的start方法。

/**
* @创建时间: 2016年1月21日
* @相关参数: @param args
* @相关参数: @return
* @相关参数: @throws Exception
* @功能描述: 对命令行参数进行解析,进行测试
* <p>
* 参数:如果什么都不传,默认测试整个类
* “ -wait ”:等待模式,测试完毕用户手动返回。
* “ -c ”:测试整个类。
* “-m ”:测试单个方法。
* “ -v ”:版本显示。
* </p>
*/
public TestResult start(String[] args) throws Exception
{
String testCase = "";
String method = "";
boolean wait = false;
for (int i = 0; i < args.length; i++)
{
if (args[i].equals("-wait"))
{
wait = true;
}
else if (args[i].equals("-c"))
{
testCase = extractClassName(args[++i]);
}
else if (args[i].equals("-m"))
{
String arg = args[++i];
int lastIndex = arg.lastIndexOf('.');
testCase = arg.substring(0, lastIndex);
method = arg.substring(lastIndex + 1);
}
else if (args[i].equals("-v"))
{
System.err.println("linkin-frame-junit测试框架版本号==> " + Version.id());
}
else
{
testCase = args[i];
}
} if (StringUtils.isBlank(testCase))
{
throw new Exception("运行参数不能为空,亲爱的!!!");
} try
{
if (StringUtils.isNotBlank(method))
{
return runSingleMethod(testCase, method, wait);
}
Test suite = getTest(testCase);
return doRun(suite, wait);
}
catch (Exception e)
{
throw new Exception("初始化测试执行器出错: " + e);
}
}

关于start方法的几个参数这里不做赘述了,具体的看我代码上面的注释。将测试类的全限定名解析后,开始构造测试组件TestSuite。通过getTest方法获取测试组件TestSuite,然后开始doRun去执行测试了。关于执行测试用例我们下篇在说,这里先来看junit初始化测试组件TestSuite这块。我们先来看看测试执行器中获取测试组件的这块代码:

/**
* @创建时间: 2016年1月21日
* @相关参数: @param suiteClassName
* @相关参数: @return
* @功能描述: 模板方法,获得suite中的测试用例
*/
public Test getTest(String suiteClassName)
{
if (suiteClassName.length() <= 0)
{
clearStatus();
return null;
}
Class<?> testClass = null;
try
{
testClass = loadSuiteClass(suiteClassName);
}
catch (ClassNotFoundException e)
{
String clazz = e.getMessage();
if (clazz == null)
{
clazz = suiteClassName;
}
runFailed("Class not found \"" + clazz + "\"");
return null;
}
catch (Exception e)
{
runFailed("Error: " + e.toString());
return null;
}
// TestSuite 的构造分两种情况
Method suiteMethod = null;
try
{
// 用户在测试类中通过声明 Suite() 方法自定义 TestSuite
suiteMethod = testClass.getMethod(SUITE_METHODNAME);
}
catch (Exception e)
{
// try to extract a test suite automatically
clearStatus();
// 自动判断并提取测试方法
return new TestSuite(testClass);
}
if (!Modifier.isStatic(suiteMethod.getModifiers()))
{
runFailed("Suite()方法必须是静态的呢");
return null;
}
Test test = null;
try
{
test = (Test) suiteMethod.invoke(null); // static method
if (test == null)
{
return test;
}
}
catch (InvocationTargetException e)
{
runFailed("Failed to invoke suite():" + e.getTargetException().toString());
return null;
}
catch (IllegalAccessException e)
{
runFailed("Failed to invoke suite():" + e.toString());
return null;
} clearStatus();
return test;
}

TestSuite 的构造分两种情况:

  • A:用户在测试类中通过声明 Suite() 方法自定义 TestSuite 。(try块中)
junit源码解析--初始化阶段

  • B:JUnit 自动判断并提取测试方法。(catch块中)

junit源码解析--初始化阶段

1,A来解释一下,JUnit 提供给用户两种构造测试集合的方法,用户既可以自行编码定义结构化的 TestCase 集合,也可以让 JUnit 框架自动创建测试集合,这种设计融合其它功能,让测试的构建、运行、反馈三个过程完全无缝一体化。

当suite方法在我们自己写的测试类中定义时,JUnit 创建一个显式的testSuite,它利用Java语言的 Reflection 机制找出名为SUITE_METHODNAME的方法,也即suite方法:

suiteMethod = testClass.getMethod(SUITE_METHODNAME);

Reflection 是 Java 的高级特征之一,借助 Reflection 的 API 能直接在代码中动态获取到类的语言编程层面的信息,如类所包含的所有的成员名、成员属性、方法名以及方法属性,而且还可以通过得到的方法对象,直接调用该方法。 JUnit 源代码频繁使用了 Reflection 机制,不仅充分发挥了 Java 语言在系统编程要求下的超凡能力,也使 JUnit 能在用户自行编写的测试类中游刃有余地分析并提取各种属性及代码,而其它测试框架需要付出极大的复杂性才能得到等价功能。

若 JUnit 无法找到 siute 方法,则抛出异常,流程进入情况 B 代码;若找到,则对用户提供的 suite 方法进行外部特征检验,判断是否为类方法。最后,JUnit 自动调用该方法,构造用户指定的 TestSuite:

test = (Test) suiteMethod.invoke(null); // static method

这里对上面这行代码解释一下,反射中method的执行API如下:

=========================================================================================================================================================================================

invoke

public Object invoke(Object obj,
Object... args)
throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException
对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。个别参数被自动解包,以便与基本形参相匹配,基本参数和引用参数都随需服从方法调用转换。

如果底层方法是静态的,那么可以忽略指定的 obj 参数。该参数可以为 null。

如果底层方法所需的形参数为 0,则所提供的 args 数组长度可以为 0 或 null。

如果底层方法是实例方法,则使用动态方法查找来调用它,这一点记录在 Java Language Specification, Second Edition 的第 15.12.4.4 节中;在发生基于目标对象的运行时类型的重写时更应该这样做。

如果底层方法是静态的,并且尚未初始化声明此方法的类,则会将其初始化。

如果方法正常完成,则将该方法返回的值返回给调用者;如果该值为基本类型,则首先适当地将其包装在对象中。但是,如果该值的类型为一组基本类型,则数组元素 被包装在对象中;换句话说,将返回基本类型的数组。如果底层方法返回类型为 void,则该调用返回 null。

参数:
obj - 从中调用底层方法的对象
args - 用于方法调用的参数
返回:
使用参数 args 在 obj 上指派该对象所表示方法的结果
抛出:
IllegalAccessException - 如果此 Method 对象强制执行 Java 语言访问控制,并且底层方法是不可访问的。
IllegalArgumentException - 如果该方法是实例方法,且指定对象参数不是声明底层方法的类或接口(或其中的子类或实现程序)的实例;如果实参和形参的数量不相同;如果基本参数的解包转换失败;如果在解包后,无法通过方法调用转换将参数值转换为相应的形参类型。
InvocationTargetException - 如果底层方法抛出异常。
NullPointerException - 如果指定对象为 null,且该方法是一个实例方法。
ExceptionInInitializerError - 如果由此方法引起的初始化失败。

=========================================================================================================================================================================================

2,B来解释一下,如果用户没有在自己的测试类中自定义suite方法,那么系统将自动创建一个suite。

return new TestSuite(testClass);

我们现在来认真看下这里,这里是junit默认的测试组件的初始化。TestSuite构造过程代码如下:

private String fName; // 测试类的类名,注意,TestCase中的fName是方法名。
private Vector<Test> fTests = new Vector<Test>(10); // 用来装用例的,可以的是TestCase,也可以是TestSuite public TestSuite(final Class<?> theClass)
{
addTestsFromTestCase(theClass);
} private void addTestsFromTestCase(final Class<?> theClass)
{
fName = theClass.getName();
try
{
getTestConstructor(theClass);
}
catch (NoSuchMethodException e)
{
addTest(warning("Class " + theClass.getName() + " has no public constructor TestCase(String name) or TestCase()"));
return;
} if (!Modifier.isPublic(theClass.getModifiers()))
{
addTest(warning("Class " + theClass.getName() + " is not public"));
return;
} Class<?> superClass = theClass;
List<String> names = new ArrayList<String>();
while (Test.class.isAssignableFrom(superClass))
{
for (Method each : MethodSorter.getDeclaredMethods(superClass))
{
addTestMethod(each, names, theClass);
}
superClass = superClass.getSuperclass();
}
if (fTests.size() == 0)
{
addTest(warning(theClass.getName() + "没有测试方法耶"));
}
} /**
* @创建时间: 2016年2月1日
* @相关参数: @param m
* @相关参数: @param names
* @相关参数: @param theClass
* @功能描述: 添加每个方法到方法List中去
*/
private void addTestMethod(Method m, List<String> names, Class<?> theClass)
{
String name = m.getName();
if (names.contains(name))
{
return;
}
if (!isPublicTestMethod(m))
{
if (isTestMethod(m))
{
addTest(warning(m.getName() + "(" + theClass.getCanonicalName() + ")方法非public修饰"));
}
return;
}
names.add(name);
addTest(createTest(theClass, name));
} // 以下2个方法是junit测试方法的约定
private boolean isPublicTestMethod(Method m)
{
return isTestMethod(m) && Modifier.isPublic(m.getModifiers());
} private boolean isTestMethod(Method m)
{
return m.getParameterTypes().length == 0 && m.getName().startsWith("test") && m.getReturnType().equals(Void.TYPE);
} /**
* @创建时间: 2016年2月1日
* @相关参数: @param message
* @相关参数: @return
* @功能描述: 约定无效然后返回一个失败的测试用例
*/
public static Test warning(final String message)
{
return new TestCase("warning")
{
@Override
protected void runTest()
{
fail(message);
}
};
} /**
* @创建时间: 2016年1月22日
* @相关参数: @param test
* @功能描述: 添加一个测试
*/
public TestSuite addTest(Test test)
{
fTests.add(test);
return this;
}

上面的代码我截取了部分源码,我们来挑选几个重要的说明以下:

1),在TestSuite构造器中调用addTestsFromTestCase方法来初始化测试组件,在addTestsFromTestCase代码中有一大亮点:

while (Test.class.isAssignableFrom(superClass))
{
for (Method each : MethodSorter.getDeclaredMethods(superClass))
{
addTestMethod(each, names, theClass);
}
superClass = superClass.getSuperclass();
}

TestSuite 采用了Composite 设计模式。在该模式下,可以将 TestSuite 比作一棵树,树中可以包含子树(其它 TestSuite),也可以包含叶子 (TestCase),以此向下递归,直到底层全部落实到叶子为止。 JUnit 采用 Composite 模式维护测试集合的内部结构,使得所有分散的
TestCase 能够统一集中到一个或若干个 TestSuite 中,同类的 TestCase 在树中占据同等的位置,便于统一运行处理。另外,采用这种结构使测试集合获得了无限的扩充性,不需要重新构造测试集合,就能使新的 TestCase 不断加入到集合中。

关于这里while循环控制的条件,我们再来看下api,实际编码中反射用的比较少,这个isAssignableFrom更是第一次遇见。

=========================================================================================================================================================================================

isAssignableFrom

public boolean isAssignableFrom(Class<?> cls)
判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口。如果是则返回 true;否则返回 false。如果该 Class 表示一个基本类型,且指定的 Class 参数正是该 Class 对象,则该方法返回 true;否则返回 false

特别地,通过身份转换或扩展引用转换,此方法能测试指定 Class 参数所表示的类型能否转换为此 Class 对象所表示的类型。有关详细信息,请参阅 Java Language Specification 的第 5.1.1 和 5.1.4 节。

参数:
cls - 要检查的 Class 对象
返回:
表明 cls 类型的对象能否赋予此类对象的 boolean 值
抛出:
NullPointerException - 如果指定的 Class 参数为 null。

=========================================================================================================================================================================================

在 TestSuite 类的代码中,可以找到该类的2个属性,

private String fName; // 测试类的类名,注意,TestCase中的fName是方法名。
private Vector<Test> fTests = new Vector<Test>(10); // 用来装用例的,可以的是TestCase,也可以是TestSuite

其中这个fTests此即为内部维护的“子树或树叶”的列表。前面的循环代码完成提取整个类继承体系上的测试方法的提取。循环语句由Class类型的实例theClass开始,逐级向父类的继承结构追溯,直到*Object类,并将沿途各级父类中所有合法的
testXXX() 方法都加入到 TestSuite中。在迭代过程中,如果方法list中已经包含该方法,则直接return。防止不同级别父类中的 testXXX() 方法重复加入 TestSuite 。

String name = m.getName();
if (names.contains(name))
{
return;
}

在每次的迭代中调用addTestMethod创建一个测试用例,添加到上面的fTests中去。下面贴出通过测试类class文件和测试方法名来创建一个测试用例的代码:

/**
* @创建时间: 2016年2月1日
* @相关参数: @param theClass 测试类
* @相关参数: @param name 方法名
* @相关参数: @return
* @功能描述: 创建一个测试用例
*/
static public Test createTest(Class<?> theClass, String name)
{
Constructor<?> constructor;
try
{
constructor = getTestConstructor(theClass);
}
catch (NoSuchMethodException e)
{
return warning("Class " + theClass.getName() + " has no public constructor TestCase(String name) or TestCase()");
}
Object test;
try
{
if (constructor.getParameterTypes().length == 0)
{
test = constructor.newInstance(new Object[0]);
if (test instanceof TestCase)
{
((TestCase) test).setName(name);
}
}
else
{
test = constructor.newInstance(new Object[] { name });
}
}
catch (InstantiationException e)
{
return (warning("Cannot instantiate test case: " + name + " (" + exceptionToString(e) + ")"));
}
catch (InvocationTargetException e)
{
return (warning("Exception in constructor: " + name + " (" + exceptionToString(e.getTargetException()) + ")"));
}
catch (IllegalAccessException e)
{
return (warning("Cannot access test case: " + name + " (" + exceptionToString(e) + ")"));
}
return (Test) test;
} /**
* @创建时间: 2016年1月22日
* @相关参数: @param theClass
* @相关参数: @return
* @相关参数: @throws NoSuchMethodException
* @功能描述: 获取测试类构造器,如果没有形参是一个字符串的构造器,则返回无参构造器
*/
public static Constructor<?> getTestConstructor(Class<?> theClass) throws NoSuchMethodException
{
try
{
return theClass.getConstructor(String.class);
}
catch (NoSuchMethodException e)
{
}
return theClass.getConstructor();
}

在这块代码中,通过反射方法newInstance获取一个测试用例,在反射的过程中也将每一个测试用例TestCase的fName设置成方法名字,注意createTest方法的那块向下强转的代码。这里强调一个事情:

TestSuite中的fName是测试组件包含的每一个测试用例的名字,TestCase中的fName是每一个测试用例的包含的测试的名字,别搞混了。

/**
* @创建作者: LinkinPark
* @创建时间: 2016年1月21日
* @功能描述: Test类的集合。
* <p>
* 1,如果自己的测试类中没有指定suite()方法,TestSuite自动初始化。
* TestSuite suite= new TestSuite(LinkinTest.class);
* 2,动态往suite中添加test。此时注意:测试类要提供一个字符串参数的的构造器,且super(method)
* TestSuite suite= new TestSuite();
* suite.addTest(new LinkinTest("testLinkin4Normal"));
* suite.addTest(new LinkinTest("testLinkin8Error"));
* 3,动态往suite中添加suite
* TestSuite suite= new TestSuite();
* suite.addTestSuite(LinkinTest.class);
* 此构造函数创建一个所有方法的套件,套件里面的每个方法以test开头并且没有任何参数。
* 4,TestSuite还可以传入一个数组
* Class[] testClasses = { LinkinTest.class, LinkinTest1.class }
* TestSuite suite= new TestSuite(testClasses);
* </p>
*/
public class TestSuite implements Test
{
private String fName; // 测试类的类名,注意,TestCase中的fName是方法名。
private Vector<Test> fTests = new Vector<Test>(10); // 用来装用例的,可以的是TestCase,也可以是TestSuite
}

public abstract class TestCase extends Assert implements Test
{
private String fName; // 测试方法的名字
}

OK,至此已经将所有的TestSuite的初始化过程全部整理完了,在执行器TestRunner中初始化TestSuite结束后就开始执行测试用例了。

Test suite = getTest(testCase);
return doRun(suite, wait);

下篇博客我会整理Junit的测试用例阶段。

最后这里以命令者设计模式在junit中的应用整理结束这篇博客。

以我们在往TestSuite中的fTest中添加测试用例的那块代码为例,我们在校验了每个测试方法是否遵循我们的测试约定之后,就开始往fTest中添加测试用例了。

addTest(createTest(theClass, name));

这行代码将 testXXX 方法转化为 TestCase,并加入到 TestSuite 。其中,addTest 方法接受 Test 接口类型的参数,其内部有 countTestCases 方法和 run 方法,该接口被 TestSuite 和 TestCase 同时实现。这是
Command 设计模式精神的体现,

Command 模式将调用操作的对象与如何实现该操作的对象解耦。在运行时,TestCase 或 TestSuite 被当作 Test 命令对象,可以像一般对象那样进行操作和扩展,也可以在实现 Composite 模式时将多个命令复合成一个命令。另外,增加新的命令十分容易,隔离了现有类的影响,今后,也可以与备忘录模式结合,实现 undo 等高级功能。