android中说TestSuite是Tests的混合物。这里的测试类,我们可以把它理解为很多的测试类。
我们可以通过TestSuite去收集一堆测试用例,然后去运行她们。
1.
android中给出了动态往TestSuite中添加测试用例的方式:
TestSuite suite= new TestSuite();
suite.addTest(new MathTest("testAdd"));
suite.addTest(new MathTest("testDivideByZero"));
先生成一个TestSuite对象,然后动态添加MathTest类的testXXX方法。
2.
除了上面的方式,TestSuite也可以把测试用例提取出来,然后去自动运行。
如果要这样做,就需要在new的时候传入包含测试用例的TestClass的类型。
TestSuite suite= new TestSuite(MathTest.class);
这个构造函数new出来的suite,包含的测试用例必须是以”test”开头,且是无参的。
我们看一下这个构造函数是怎么样的:
public TestSuite(final Class<?> theClass) {
addTestsFromTestCase(theClass);
}
里面有一个addTestsFromTestCase方法,从字面的意思理解:就是从对应的测试类中提取test测试用例。
我们来看一下它的具体实现,并且分析一下:
private void addTestsFromTestCase(final Class<?> theClass) {
fName= theClass.getName();
try {
getTestConstructor(theClass); // Avoid generating multiple error messages
} 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 : superClass.getDeclaredMethods())
addTestMethod(each, names, theClass);
superClass= superClass.getSuperclass();
}
if (fTests.size() == 0)
addTest(warning("No tests found in "+theClass.getName()));
}
设置fName为测试类名,然后会通过getTestConstructor对传入的测试类做一个检测,检测是否有带String类型参数的公有构造函数或者是无参的公有构造函数。也就是说,
这里限定了测试员在写一个测试类的时候必须声明一个public constructor TestCase(String name)或者TestCase(),否则会给你抛出一个警告。
接下去判断测试类是否是一个公有类型的Class。
接着通过一个while循环,判断superClass是否可以转换为Test类型,我们知道android测试中定义的测试类一般都会继承AndroidTestCase或其他XXXTestCase,
而这些TestCase都实现了Test接口。所以通过这样一种方式,就可以去遍历继承体系中的所有方法,把需要的测试用例TestSuite中。最后在superClass为Test后,
在去getSuperclass,这时superClass就为Test的SuperClass,退出while循环。
上面讲到,我们声明的测试用例必须是公有无参且以”test”开头的,那么这在哪里检测呢?这就来看一下addTestMethod:
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("Test method isn't public: "+ m.getName() + "(" + theClass.getCanonicalName() + ")"));
return;
}
names.add(name);
addTest(createTest(theClass, name));
}
会在isPublicTestMethod中判断是否公有无参且以”test”开头,如果否,会判断一下是否是”test”开头且无参,如果是,就告诉用户需要把测试用例声明为public(多么人性化
的代码,努力学习中。。。)
如果符合要求,我们就把方法名添加到一个List<String>中,避免重复添加。然后通过addTest去添加测试用例—根据测试类名和测试用例名去动态的生成一个Test。
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;
}
根据用户TestClass构造函数不同回去生成以测试用例命名的Test。如果是无参的,会调用setName去设置;如果有参,会往newInstance中传入new Object[]{name}。
那么Test存放在哪里呢?
private Vector<Test> fTests= new Vector<Test>(10);
我们看到前面声明了一个Vector,所有Test都是保存在这个Vector中。
public void addTest(Test test) {
fTests.add(test);
}
addTestsFromTestCase的最后会判断fTests.size()时候等于0,如果等于0,就说明没有找到相关的Test。
3.
我们也可以定义一个Class[]对象作为参数传入构造函数,里面包含各种测试类。
Class[] testClasses = { MathTest.class, AnotherTest.class }
TestSuite suite= new TestSuite(testClasses);
这里以Class[]数组类型为参数的构造函数,实际也只是循环调用2中的构造函数,如下:
public TestSuite (Class<?>... classes) {
for (Class<?> each : classes)
addTest(testCaseForClass(each));
}
private Test testCaseForClass(Class<?> each) {
if (TestCase.class.isAssignableFrom(each))
return new TestSuite(each.asSubclass(TestCase.class));
else
return warning(each.getCanonicalName() + " does not extend TestCase");
}
通过testCaseForClass会把对应的TestCase派生类封装成一个新的TestSuite对象,然后返回该对象。
从这里我们就明白了junit管理测试用例的框架流程:
(1)当只有一个测试类的时候,就把该测试类封装成TestSuite,然后去运行里面的测试用例。
(2)当有多个测试类的时候,就把每个测试类封装成TestSuite,然后再把这些TestSuite封装成一个TestSuite,运行的时候就去循环运行每个TestSuite中的TestCase。
4.
讲完了怎么生成一个TestSuite,那么TestSuite中包含的Test是怎么被运行的呢。
我们这里假设Test是放在InstrumentationTestRunner中运行,首先进入onCreate:
public void onCreate(Bundle arguments) {
。。。。。。。
mTestRunner = getAndroidTestRunner();
mTestRunner.setContext(getTargetContext());
mTestRunner.setInstrumentation(this);
mTestRunner.setSkipExecution(logOnly);
mTestRunner.setTest(testSuiteBuilder.build());
。。。。。。
}
我们会看到这里会获取到一个AndroidTestRunner对象,然后在其中运行测试用例,
mTestRunner.setTest(testSuiteBuilder.build())就是设置要在AndroidTestRunner中运行的TestSuite。
那么在setTest中做了哪些操作?
public void setTest(Test test) {
setTest(test, test.getClass());
}
private void setTest(Test test, Class<? extends Test> testClass) {
mTestCases = (List<TestCase>) TestCaseUtil.getTests(test, true);
if (TestSuite.class.isAssignableFrom(testClass)) {
mTestClassName = TestCaseUtil.getTestName(test);
} else {
mTestClassName = testClass.getSimpleName();
}
}
看到会用一个mTestCases去保存所有TestCase,然后会在InstrumentationTestRunner的onStart中去执行runTest(),逐个运行所有TestCase。
这里我们来看看TestCaseUtil是怎么获取TestCase列表的?
public static List<? extends Test> getTests(Test test, boolean flatten) {
return getTests(test, flatten, new HashSet<Class<?>>());
}
private static List<? extends Test> getTests(Test test, boolean flatten,
Set<Class<?>> seen) {
List<Test> testCases = Lists.newArrayList();
if (test != null) {
Test workingTest = null;
/*
* If we want to run a single TestCase method only, we must not
* invoke the suite() method, because we will run all test methods
* of the class then.
*/
if (test instanceof TestCase &&
((TestCase)test).getName() == null) {
workingTest = invokeSuiteMethodIfPossible(test.getClass(),
seen);
}
if (workingTest == null) {
workingTest = test;
}
if (workingTest instanceof TestSuite) {
TestSuite testSuite = (TestSuite) workingTest;
Enumeration enumeration = testSuite.tests();
while (enumeration.hasMoreElements()) {
Test childTest = (Test) enumeration.nextElement();
if (flatten) {
testCases.addAll(getTests(childTest, flatten, seen));
} else {
testCases.add(childTest);
}
}
} else {
testCases.add(workingTest);
}
}
return testCases;
}
从上面的代码我们看到,只要传入的test不是null,这里就会分两种方式来去获取testCases。
第一种,判断test是不是TestCase的对象且没有设置名称,这个时候就会尽可能的去调用对应Class中的suite()方法,如果不存在该方法,就返回null。
第二种,判断test是否是TestSuite的对象,然后通过一个while循环去遍历TestSuite集,接着通过设置flattern的值,判断是否需要递归调用getTests去获取testcases。
public void runTest(TestResult testResult) {
mTestResult = testResult;
for (TestListener testListener : mTestListeners) {
mTestResult.addListener(testListener);
}
Context testContext = mInstrumentation == null ? mContext : mInstrumentation.getContext();
for (TestCase testCase : mTestCases) {
setContextIfAndroidTestCase(testCase, mContext, testContext);
setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
setPerformanceWriterIfPerformanceCollectorTestCase(testCase, mPerfWriter);
testCase.run(mTestResult);
}
}
这里看到for循环里会调用testCase自己的run方法,运行结果放在TestResult中。实际上这里最终会调用到:
protected void runTest() throws Throwable {
assertNotNull("TestCase.fName cannot be null", fName); // Some VMs crash when calling getMethod(null,null);
Method runMethod= null;
try {
// use getMethod to get all public inherited
// methods. getDeclaredMethods returns all
// methods of this class but excludes the
// inherited ones.
runMethod= getClass().getMethod(fName, (Class[])null);
} catch (NoSuchMethodException e) {
fail("Method \""+fName+"\" not found");
}
if (!Modifier.isPublic(runMethod.getModifiers())) {
fail("Method \""+fName+"\" should be public");
}
try {
runMethod.invoke(this);
}
catch (InvocationTargetException e) {
e.fillInStackTrace();
throw e.getTargetException();
}
catch (IllegalAccessException e) {
e.fillInStackTrace();
throw e;
}
}
通过反射机制去获取类中公有继承的方法,然后通过invoke去调用。