本文接上文介绍基于junit的android测试框架。
5、AndroidTestRunner
随着学习的深入,发现包在前面的篇幅中,我们忽略了android.test包中一个重要的类AndroidTestRunner,这个类是android.test包的核心类,下面为大家详细说明,并补充说明一些相关的内容。
junit.framework包中的TestListener接口
这个接口的函数,列举如下:
package junit.framework;
/**
* A Listener for test progress
*/
public interface TestListener {
/**
* An error occurred.
*/
public void addError(Test test, Throwable t);
/**
* A failure occurred.
*/
public void addFailure(Test test, AssertionFailedError t);
/**
* A test ended.
*/
public void endTest(Test test);
/**
* A test started.
*/
public void startTest(Test test);
}
与这个接口,相关的类就只用TestResult,相关接口如下:
/**
* Registers a TestListener
*/
public synchronized void addListener(TestListener listener) {
fListeners.addElement(listener);
}
/**
* Unregisters a TestListener
*/
public synchronized void removeListener(TestListener listener) {
fListeners.removeElement(listener);
}
看到这里就应该知道如何使用了,具体的使用在下一篇幅例子中说明。
junit.runner包,结构如下:
这是一个对junit.framework的辅助包,包主要就是BaseTestRunner类,其实现了TestListener接口,主要功能是:对测试过程中Error、Failure的检查。
有了这些补充说明,下面学习android.test包中一个重要的类AndroidTestRunner。
AndroidTestRunner类结构,如下图所示:
其主要接口函数,列举如下:
看到setContext(Context context)这个函数的这个参数Context context,总算让我看到junit与Android的结合点了,在看下其他几个函数,我们会发现,这个类是android.test的核心控制类,大家心中的疑惑顿时就没有了。列举一个简要的例子,如下:
AndroidTestRunner testRunner = new AndroidTestRunner();
testRunner.setTest( new ExampleSuite() );
testRunner.addTestListener( this );
testRunner.setContext( parentActivity );
testRunner.runTest();
通过AndroidTestRunner控制整个测试,并与我们的Activity向结合
。
6、androidTest例子分析
前面我们学习了android.test包中的大部分类,是该通过学习具体的例子将前面的知识融会贯通,让我们的理解更加深刻,例子程序代码下载地址,下载后添加Eclipes的工程中,边看这篇文章边阅读例子程序的代码。
首先分析整个工程的结构图,如下:
AndroidTestCase,Testsuite在前面的篇幅中已经学习过了,ContestTest、MathTest、SomeTest、ExampleSuite在前面的例子中已经为大家介绍了,这里我们主要说明整个程序是如何运行的?
android界面程序如下:
public class JUnit extends Activity {
static final String LOG_TAG = "junit";
Thread testRunnerThread = null;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button launcherButton = (Button)findViewById( R.id.launch_button );
launcherButton.setOnClickListener( new View.OnClickListener() {
public void onClick( View view ) {
startTest();
}
} );
}
private synchronized void startTest()
{
if( ( testRunnerThread != null ) &&
!testRunnerThread.isAlive() )
testRunnerThread = null;
if( testRunnerThread == null ) {
testRunnerThread = new Thread( new TestRunner( this ) );
testRunnerThread.start();
} else
Toast.makeText(
this,
"Test is still running",
Toast.LENGTH_SHORT).show();
}
}
class TestDisplay implements Runnable
{
public enum displayEvent{START_TEST,END_TEST,ERROR,FAILURE,}
displayEvent ev;
String testName;
int testCounter;
int errorCounter;
int failureCounter;
TextView statusText;
TextView testCounterText;
TextView errorCounterText;
TextView failureCounterText;
public TestDisplay( displayEvent ev,
String testName,
int testCounter,
int errorCounter,
int failureCounter,
TextView statusText,
TextView testCounterText,
TextView errorCounterText,
TextView failureCounterText )
{
this.ev = ev;
this.testName = testName;
this.testCounter = testCounter;
this.errorCounter = errorCounter;
this.failureCounter = failureCounter;
this.statusText = statusText;
this.testCounterText = testCounterText;
this.errorCounterText = errorCounterText;
this.failureCounterText = failureCounterText;
}
public void run()
{
StringBuffer status = new StringBuffer();
switch( ev ) {
case START_TEST:
status.append( "Starting" );
break;
case END_TEST:
status.append( "Ending" );
break;
case ERROR:
status.append( "Error: " );
break;
case FAILURE:
status.append( "Failure: " );
break;
}
status.append( ": " );
status.append( testName );
statusText.setText( new String( status ) );
testCounterText.setText( "Tests: "+testCounter );
errorCounterText.setText( "Errors: "+errorCounter );
failureCounterText.setText( "Failure: "+failureCounter );
}
}
class TestRunner implements Runnable,TestListener
{
static final String LOG_TAG = "TestRunner";
int testCounter;
int errorCounter;
int failureCounter;
TextView statusText;
TextView testCounterText;
TextView errorCounterText;
TextView failureCounterText;
Activity parentActivity;
public TestRunner( Activity parentActivity )
{
this.parentActivity = parentActivity;
}
public void run()
{
testCounter = 0;
errorCounter = 0;
failureCounter = 0;
statusText = (TextView)parentActivity.
findViewById( R.id.status );
testCounterText = (TextView)parentActivity.
findViewById( R.id.testCounter );
errorCounterText = (TextView)parentActivity.
findViewById( R.id.errorCounter );
failureCounterText = (TextView)parentActivity.
findViewById( R.id.failureCounter );
Log.d( LOG_TAG, "Test started" );
AndroidTestRunner testRunner = new AndroidTestRunner();
testRunner.setTest( new ExampleSuite() );
testRunner.addTestListener( this );
testRunner.setContext( parentActivity );
testRunner.runTest();
Log.d( LOG_TAG, "Test ended" );
}
// TestListener
public void addError(Test test, Throwable t)
{
Log.d( LOG_TAG, "addError: "+test.getClass().getName() );
Log.d( LOG_TAG, t.getMessage(), t );
++errorCounter;
TestDisplay td = new TestDisplay(
TestDisplay.displayEvent.ERROR,
test.getClass().getName(),
testCounter,
errorCounter,
failureCounter,
statusText,
testCounterText,
errorCounterText,
failureCounterText );
parentActivity.runOnUiThread( td );
}
public void addFailure(Test test, AssertionFailedError t)
{
Log.d( LOG_TAG, "addFailure: "+test.getClass().getName() );
Log.d( LOG_TAG, t.getMessage(), t );
++failureCounter;
TestDisplay td = new TestDisplay(
TestDisplay.displayEvent.FAILURE,
test.getClass().getName(),
testCounter,
errorCounter,
failureCounter,
statusText,
testCounterText,
errorCounterText,
failureCounterText );
parentActivity.runOnUiThread( td );
}
public void endTest(Test test)
{
Log.d( LOG_TAG, "endTest: "+test.getClass().getName() );
TestDisplay td = new TestDisplay(
TestDisplay.displayEvent.END_TEST,
test.getClass().getName(),
testCounter,
errorCounter,
failureCounter,
statusText,
testCounterText,
errorCounterText,
failureCounterText );
parentActivity.runOnUiThread( td );
}
public void startTest(Test test)
{
Log.d( LOG_TAG, "startTest: "+test.getClass().getName() );
++testCounter;
TestDisplay td = new TestDisplay(
TestDisplay.displayEvent.START_TEST,
test.getClass().getName(),
testCounter,
errorCounter,
failureCounter,
statusText,
testCounterText,
errorCounterText,
failureCounterText );
parentActivity.runOnUiThread( td );
}
}
整个程序的核心代码,如下:
AndroidTestRunner testRunner = new AndroidTestRunner();
testRunner.setTest( new ExampleSuite() );
testRunner.addTestListener( this );
testRunner.setContext( parentActivity );
testRunner.runTest();
AndroidTestRunner这个核心类,在前面的篇幅中我们已经学习过,再次回忆下这张图(在大脑中留下深刻的记忆,后面会经常使用):
红色划线部分代表例子程序代码中使用的AndroidTestRunner类的函数。这里使用单独线程的主要作用就是:testRunner.runTest();会占用大量的时间,如果直接在UI线程中运行会阻滞UI线程,导致界面停止反应,这对用户的操作会有很大的影响。
如何将TestRunner 中的测试信息显示在界面上?
Android SDK为我们提供了Handler,通过Handler与一个线程的消息队列相关联,发送和处理信息。在这个例子中使用了Activity类的runOnUiThread (Runnable action)函数,这个函数的主要功能:在UI线程中运行指定的操作,如果当前线程是UI线程,然后采取行动立即执行;如果当前线程不是UI线程,发送消息到UI线程的事件队列。
整个程序就介绍完了,运行程序后的界面如下:
在这里需要特殊说明的是:打开AndroidManifest.xml文件,发现<application>有个以前没有见过的标记,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="aexp.junit"
android:versionCode="1"
android:versionName="1.0.0">
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<application android:label="@string/app_name">
<uses-library android:name="android.test.runner"/>
<activity android:name=".JUnit"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="4" />
</manifest>
总结说明
这个例子已经学习完了,虽然它比较简单,但是让我们清晰的了解如何使用AndroidTestRunner,后面我们将继续介绍一些复杂的例子,更加深入的学习。
参考文献:
Android、JUnit深入浅出系列文章