Android 테스트 3 --TestSuite 소스 분석.
10855 단어 안드로이드 개발 및 테스트
우리는 Test Suite를 통해 테스트 용례를 한 무더기 수집한 후에 그녀들을 운행할 수 있다.
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 방법이 있는데 글자의 뜻에서 이해할 수 있다. 바로 대응하는 테스트 클래스에서 테스트 용례를 추출하는 것이다.
구체적인 구현을 살펴보고 분석해 보겠습니다.
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 names= new ArrayList();
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 형식 파라미터가 있는 공유 구조 함수나 참조가 없는 공유 구조 함수가 있는지 검사합니다.즉,테스트원이 테스트 클래스를 작성할 때 퍼블릭 constructor Test Case (String name) 나 Test Case () 를 명시해야 하며, 그렇지 않으면 경고를 던질 수 있습니다.
이어서 테스트 클래스가 공유 유형의 클래스인지 판단합니다.
이어서while 순환을 통해 슈퍼클래스가Test 유형으로 전환될 수 있는지 판단합니다.android 테스트에서 정의한 테스트 클래스는 일반적으로 안드로이드 TestCase 또는 다른 XXXTestCase를 계승하는 것으로 알고 있습니다.
이러한 TestCase는 Test 인터페이스를 구현합니다.그래서 이런 방식을 통해 계승 체계의 모든 방법을 두루 훑어보고 필요한 테스트 용례인Test Suite를마지막으로 슈퍼클래스가 Test인 후,
get Superclass로 가면 슈퍼클래스는Test의 슈퍼클래스이고while 순환을 종료합니다.
위에서 말한 바와 같이 우리가 성명한 테스트 용례는 반드시 공유무참이고'test'로 시작해야 한다. 그러면 이것은 어디에서 측정되는 것입니까?그럼 addTestMethod:
private void addTestMethod(Method m, List 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'로 시작하고 무참인지 판단하고, 만약 그렇다면 사용자에게 테스트용례를'test'로 명시해야 한다(얼마나 인성화된 것인지)
코드, 열심히 공부하는 중...
만약 요구에 부합된다면, 우리는 방법명을 하나의 리스트에 추가하여, 중복 추가를 피합니다.그리고ddTest를 통해 테스트 용례를 추가합니다. 테스트 클래스 이름과 테스트 용례 이름에 따라 동적으로 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을 호출하여 설정합니다.참조가 있으면 new Instance에 new Object[] {name} 이 전송됩니다.
그럼 Test는 어디에 보관할까요?
private Vector fTests= new Vector(10);
앞에서 설명한 Vector를 보았습니다. 모든 Test는 이 Vector에 저장되어 있습니다.public void addTest(Test test) {
fTests.add(test);
}
addTestsFromTestCase의 마지막 판단 fTests.size () 는 0이고 0이면 관련 Test를 찾을 수 없습니다.
3.
우리도 클라스[] 대상을 매개 변수로 구조 함수로 정의할 수 있는데 그 안에 각종 테스트 클래스가 포함되어 있다.
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) 테스트 클래스가 하나일 때 이 테스트 클래스를Test Suite로 봉하여 안에 있는 테스트 용례를 실행한다.
(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());
。。。。。。
}
안드로이드 Test Runner 대상을 가져와서 테스트 용례를 실행하는 것을 볼 수 있습니다.
mTestRunner.setTest (test Suite Builder.build () 는 안드로이드 Test Runner에서 실행할 Test Suite를 설정합니다.
그러면 setTest에서 어떤 조작을 했나요?
public void setTest(Test test) {
setTest(test, test.getClass());
}
private void setTest(Test test, Class extends Test> testClass) {
mTestCases = (List) 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>());
}
private static List extends Test> getTests(Test test, boolean flatten,
Set> seen) {
List 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;
}
위의 코드에서 보듯이 전송된 테스트가null이 아니라면 두 가지 방식으로 테스트 케이스를 가져옵니다.
첫 번째,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를 통해 호출합니다.