Android SDK 1.5已经将JUnit包含进来了,但是一直没有去深入了解,以前在使用一些C++的开源库中学习过与CPPUnit,简要分析过其主要框架,如下:
这次在学习Android SDK 1.6中的例子程序APIDemoes中的过程中,看到了一个test文件夹,似乎就是使用了JUnit,于是就开始学习Android中如何使用JUnit。APIDemoes\test文件夹下的测试代码相对比较复杂,我们会循序渐进的学习,逐步深入的理解,在后面为大家详细解析如何阅读这些测试代码,本篇幅就初步解析JUnit。
什么是 JUnit ?
JUnit是采用测试驱动开发的方式,也就是说在开发前先写好测试代码,主要用来说明被测试的代码会被如何使用,错误处理等;然后开始写代码,并在测试代码中逐步测试这些代码,直到最后在测试代码中完全通过。
现简要说JUnit的4大功能
1. 管理测试用例。修改了哪些代码,这些代码的修改会对哪些部分有影响,通过JUnit将这次的修改做个完整测试。这也就JUnit中所谓的TestSuite。
2. 定义测试代码。这也就是JUnit中所谓的TestCase,根据源代码的测试需要定义每个TestCase,并将TestCase添加到相应的TestSuite方便管理。
3. 定义测试环境。在TestCase测试前会先调用“环境”配置,在测试中使用,当然也可以在测试用例中直接定义测试环境。
4. 检测测试结果。对于每种正常、异常情况下的测试,运行结果是什么、结果是否是我们预期的等都需要有个明确的定义,JUnit在这方面提供了强大的功能。
以上部分与我们平常使用IDE调试的过程是完全一样的,只不过是增加了测试用例管理、测试结果检测等功能,提高了单元的效率,保证了单元测试的完整性,明确了单元测试的目标。
以上4大功能,在JUnit的框架中是如何实现的了,在下一篇幅JUnit例子分析中,通过一个简要的例子,详细说明4大功能是如何实现的。
我们简要了解了JUnit的主要功能:
1. 管理测试用例;
2. 定义测试代码;
3. 定义测试环境;
4. 检测测试结果;
结合主要功能,举个简单的例子分析如下:
源代码:
public class SampleCalculator
{
public int add(int augend , int addend)
{return augend + addend ;}
public intsubtration(int minuend , int subtrahend)
{ return minuend– subtrahend ;}
}
测试用例(TestCase):
import junit.framework.TestCase;
public class TestSample extends TestCase
{
private int a;
private int b;
private int r1,r2;
void setUp() /*开始测试当前用例–初始化测试环境*/
{
a = 50;
b = 20;
r1 = 70;
r2 = 30;
}
voidtearDown()/*当期用例测试结束*/
{}
public voidtestAdd()/*测试SampleCalculator 类的Add函数*/
{
SampleCalculator calculator = new SampleCalculator();
int result = calculator.add(a , b);
assertEquals(r1 , result);/*检测测试结果*/
}
public voidtestSubtration()/*测试SampleCalculator类的Subtration函数*/
{
SampleCalculator calculator = new SampleCalculator();
int result = calculator.subtration(a , b);
assertEquals(r2 , result);/*检测测试结果*/
}
}
以上TestSample测试用例中就对SampleCalculator进行了完整的单元测试,并对测试结果做了预期说明。
TestCase的管理
import junit.framework.Test;
import junit.framework.TestSuite;
public class TestAll
{
public static Test suite()
{
TestSuite suite = new TestSuite(“TestSuite Test”);
suite.addTestSuite(TestSample.class);
return suite;
}
}
以上就将TestSample增加到“TestSuite Test”中,将来在选择测试用例的过程中只要选择了TestSuite Test,TestSample就将加入当前测试中。如果将来SampleCalculator增加了其他功能,只需要在TestSample增加相应的测试代码。
最后需要说明的:对TestCase的管理,是完全界面化的,JUnit会自动产生UI界面,运行以上测试的例子,JUnit的界面如下:
可能还需要下载JUint packeage,最后送给大家一句话:大胆尝试下,你会发现编程真的可以如此“美好”。
通过前面2篇文章的学习,我们对JUnit有了初步的认识,下面我们将深入的解析JUnit数据包。整个JUnit的数据包应该是很强大的,但是一般来说,不一定每个工程都需要这些数据包,而是在JUnit部分数据包的基础上扩展出自己的数据包,Android SDK中也不例外。至于JUnit完整的包,这里我们就不详细分析了,我们这里只解析Android SDK中包含的那些JUnit数据包,以及Android SDK在JUnit的基础上扩展的一些数据包,如下:
SDK |
功能说明 |
junit.framework |
JUnit测试框架 |
junit.runner |
实用工具类支持JUnit测试框架 |
android.test |
Android 对JUnit测试框架的扩展包 |
android.test.mock |
Android的一些辅助类 |
android.test.suitebuilder |
实用工具类,支持类的测试运行 |
在这些包中最为重要的是:junit.framework、android.test,其中前者是JUnit的核心包,后者是Andoid SDK在Junit.framework的基础上扩展出来的包,我们将重点解析这2个包。
首先解析junit.framework包,结构如下:
通过这张图,大家就可以比较清晰的看到JUnit的主要框架,再回去看下上篇文章的例子,对前面的例子感觉明白多了。做个简要的总结,如下:
1. TestSuit:TestSuite是测试用例的集合;
2. TestCase:定义运行多个测试用例;
3. TestResult:收集一个测试案例的结果,测试结果分为失败和错误,如果未能预计的断言就是失败,错误就像一个ArrayIndexOutOfBoundsException异常而导致的无法预料的问题;
4. TestFailure:测试失败时捕获的异常;
5. Assert:断言的方法集,当断言失败时显示信息;
TestCase与TestSuite之间的关系,有些类似于图元对象与容器对象之间的关系,在面向对象的语言C++、JAVA中较常见,在这里就不多说了。
举个简单的例子,并简要说明过程
第一步:实现TestCase
1. 继承父类TestCase;
2. 定义一下变量在测试中使用;
3. 在setUp()中初始化这些变量;
4. 在tearDown()中清理这些变量;
public class MathTest extends TestCase{
protected double fValue1;
protected double fValue2;
protected void setUp(){
fValue1= 2.0;
fValue2= 3.0;
}
}
5. 编写测试单元代码;
public void testAdd() {
double result= fValue1 + fValue2;
assertTrue(result == 5.0);
}
6. 运行测试用例,这里有2种方法可以使用:
· 静态类型:覆盖runTes()和定义测试函数。最常用的就是采用java的匿名类,如下:
TestCase test= new MathTest(“add”){
public void runTest() { testAdd();}
};
test.run();
· 动态类型:使用反射来实现runTest,它动态地发现并调用的方法,在这种情况下,测试案例的名字对应的测试方法来运行,如下:TestCase= new MathTest(“testAdd”);
test.run();
相比之下,第2种更符合面向对象的思维。
第二步:将TestCase添加到TestSuilt
TestSuite suite= new TestSuite();
suite.addTest(newMathTest("testAdd"));
由于TestSuite可以自动从TestCase中提取测试单元并运行,也可以用如下方法:
TestSuite suite= newTestSuite(MathTest.class);
一个测试用例就完成了,想要更加详细的了解junit.framework,还是到Android SDK中仔细阅读。
总结说明
看了这些代码,再仔细看下JUnit的结构图,是不是感觉更加清晰了,下一篇幅我们将深入解析android.test包。
前面我们学习了junit.framework包,本篇幅我们开始学习android.test包,了解Android SDK是如何扩展junit.framework包。
在前面的篇幅中说明过:TestCase与TestSuite之间的关系,有些类似于图元对象与容器对象之间的关系。有了这个思想,顺着这张的主线图从上往下看:首先看到的是TestCase类,接着看到InstrumentationTestCase、AndroidTestCase,最后看到ApplicationTestCase,ProviderTestCase2,ServiceTestCase,ActivityTestCase。这不就是Android 系统中4大组成元素:Activity、Provider、Service、BroadCast中的前3个,然后我们在看下Android SDK中的说明:
类 |
说明 |
AndroidTestCase |
如果你要访问资源或其他东西依赖于Activity的环境,在这个类的基础上扩展。 |
ActivityInstrumentationTestCase2 |
这个类提供了一个单一的活动功能测试 |
ApplicationTestCase |
提供了一个框架,可以在受控环境中测试Application类 |
ProviderTestCase2 |
提供了一个框架,可以在受控环境中测试ContentProvider类 |
ServiceTestCase |
提供了一个框架,可以在受控环境中测试ServiceTest类。 |
这些类就不在这里说明,应该与我们平时使用Activity、Service、Provider基本一样。下面举个例子,来说明如何使用这些类,我们就将上一篇J2SE测试例子,在Android中实现:
//MathTest.java
importandroid.test.AndroidTestCase;
importandroid.util.Log;
publicclassMathTest extendsAndroidTestCase
{
protecteddoublefValue1;
protecteddoublefValue2;
protecteddoublefRe;
staticfinalString LOG_TAG= “MathTest”;
protectedvoidsetUp() {
fValue1= 2.0;
fValue2= 3.0;
fRe = 5.0;
}
public voidtestAdd()
{
Log.d( LOG_TAG, “testAdd” );
assertTrue(LOG_TAG+”1″, ( (fValue1 + fValue2 ) == fRe ) );
}
}
//ExampleSuite.java
importjunit.framework.TestSuite;
publicclassExampleSuite extendsTestSuite
{
publicExampleSuite()
{
addTestSuite( MathTest.class );
}
}
与上一篇的例子比较后发现,除了引用的包、父类不一样以外,其他部分是完全一样的,在Android AVD上运行程序后看到的结果如下:
单击按钮“Launch test”运行测试用例,输出测试结果,我们的测试都通过了。这个测试界面是我们自己编写的单元测试的界面,下一篇幅我们将重点介绍这部分。
总结说明
这里我们简要学习了android.test包,其实这个的内容远不止这些,包含:3个Interface、20个class、2个Error,列举如下:
android.test包深入的学习,只有到Android SDK中去仔细阅读了。通过学习android.test,让我们对Android 系统的组成元素:Activity、Provider、Service有了更加深入的了解。
随着学习的深入,发现包在前面的篇幅中,我们忽略了android.test包中一个重要的类AndroidTestRunner,这个类是android.test包的核心类,下面为大家详细说明,并补充说明一些相关的内容。
这个接口的函数,列举如下:
与这个接口,相关的类就只用TestResult,相关接口如下:
看到这里就应该知道如何使用了,具体的使用在下一篇幅例子中说明。
这是一个对junit.framework的辅助包,包主要就是BaseTestRunner类,其实现了TestListener接口,主要功能是:对测试过程中Error、Failure的检查。
有了这些补充说明,下面学习android.test包中一个重要的类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向结合,具体的使用在下一篇幅中详细说明。
前面我们学习了android.test包中的大部分类,是该通过学习具体的例子将前面的知识融会贯通,让我们的理解更加深刻,例子程序代码下载地址,下载后添加Eclipes的工程中,边看这篇文章边阅读例子程序的代码。
AndroidTestCase,Testsuite在前面的篇幅中已经学习过了,ContestTest、MathTest、SomeTest、ExampleSuite在前面的例子中已经为大家介绍了,这里我们主要说明整个程序是如何运行的?
publicclass 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();
}
classTestRunner implements Runnable,TestListener
{
static final String LOG_TAG = “TestRunner”;
int testCounter;
int errorCounter;
int failureCounter;
……;
Activity parentActivity;
publicTestRunner( Activity parentActivity )
{this.parentActivity = parentActivity;}
publicvoid run()
{
testCounter = 0;
errorCounter = 0;
failureCounter = 0;
………….;
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;
…….;
}
publicvoid addFailure(Test test, AssertionFailedError t)
{
Log.d( LOG_TAG, “addFailure: “+test.getClass().getName() );
Log.d( LOG_TAG, t.getMessage(), t );
++failureCounter;
…….;
}
publicvoid endTest(Test test)
{
Log.d( LOG_TAG, “endTest: “+test.getClass().getName() );
…..;
}
publicvoid startTest(Test test)
{
Log.d( LOG_TAG, “startTest: “+test.getClass().getName() );
++testCounter;
…….;
}
}
通过将源工程中的代码简单整理后,就可以看到TestRunner这个工作者线程(window中的术语,没有界面的线程)的作用,这让我们对TestListener有了更加深入的了解。
publicvoid run()
{
……;
/*整个代码的核心*/
AndroidTestRunner testRunner = new AndroidTestRunner();
testRunner.setTest( new ExampleSuite() );
testRunner.addTestListener( this );
testRunner.setContext( parentActivity );
testRunner.runTest();
…….;
}
AndroidTestRunner这个核心类,在前面的篇幅中我们已经学习过,再次回忆下这张图(在大脑中留下深刻的记忆,后面会经常使用):
红色划线部分代表例子程序代码中使用的AndroidTestRunner类的函数。这里使用单独线程的主要作用就是:testRunner.runTest();会占用大量的时间,如果直接在UI线程中运行会阻滞UI线程,导致界面停止反应,这对用户的操作会有很大的影响。
在前面的Snake例子程序中介绍过:Android SDK为我们提供了Handler,通过Handler与一个线程的消息队列相关联,发送和处理信息。在这个例子中使用了Activity类的runOnUiThread (Runnable action)函数,这个函数的主要功能:在UI线程中运行指定的操作,如果当前线程是UI线程,然后采取行动立即执行;如果当前线程不是UI线程,发送消息到UI线程的事件队列。
整个程序就介绍完了,运行程序后的界面如下:
在这里需要特殊说明的是:打开AndroidManifest.xml文件,发现<application>有个以前没有见过的标记,如下:
<application android:label=”@string/app_name”>
<uses-libraryandroid: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>
user-library元素说明:指定一个共享库,应用程序需要连接的。默认情况下会连接所有的Android库,然而一些软件包(如地图和AWT)是不会自动连接独立的库,以确定哪些库需要包含这些特定的包代码文件。
这个例子已经学习完了,虽然它比较简单,但是让我们清晰的了解如何使用AndroidTestRunner,后面我们将继续介绍一些复杂的例子,更加深入的学习。
前面我们学习了很多JUnit的程序,在Android ADT插件中已经为我们提供了很多这方面的功能,方便我们进行单元测试。但是如何进行单元测试,例如在ApiDemos\test提供的测试例子程序如何运行,对于我们这些初学者来说有些茫然,我也是在网上查找了不少这方面的的资料学习,才知道如何运行测试单元,因此在这里总结说明与大家分享。总结起来,大概有4种不同的方法:
在Eclipes中选择工程,单击右键,在Run as/Debug as子菜单选项中选择Android JUnit Test,如下:
单击运行后,应用程序将启动,在Eclipes中会出现个新的面板JUnit,如下:
这个界面上就显示了测试的结果,这种方法操作比较简单,但是要想自己写单元测试就的必须深入的去了解后面2中运行的方法。
运行AVD,选择Dev Tool,当前界面如下:
双击Instrumentation后,界面如下:
测试例子开始运行,在LogCat中查看运行过程输出的信息,界面如下:
这些信息就是测试例子输出的信息。
这种方法应该是为linux程序员设置的,完全的命令行,使用起来相对比较麻烦,命令格式如下:
按照上面的命令行格式,输入:adbshell am instrument -w com.xmobileapp.hello/android.test.InstrumentationTestRunner
运行后的界面如下:
测试结果的信息与第2种方法中的logCat中输出的信息是完全一样的。
在运行测试例子的过程中,也会遇到了不少的错误提示,总结如下:
单击“AndroidJUnit Test”运行后,出现“Android Launch”错误提示,如下:
这个错误的原因估计是:AndroidManifest.xml配置错误,关于这个问题的分析说明,请阅读An instrumention test runner is notspecified篇幅中的说明。
使用adbshell am命令运行,出现“Error =Unable to find instrumentation info for……..”错误信息,如下:
具体的原因是什么还不知道,如果有谁知道的,在评论中说明下。
看了这些,对instrument是不是有很大的困惑,下一篇幅我们将学习AndroidSDK 中的instrument。为了方便大家学习上面的运行测试单元的方法,一个简单的例子供大家下载,这个例子中包含一个Activity(Hello)以及对这个Activity(HelloTest)的单元测试2部分,大家可以使用上面介绍的方法来启动单元测试。
在学习Android、JUnit的过程中,随着学习的深入,将Android、JUnit的类按照继承关系整理如下:
上面的5条路线,也是我们不断学习的过程,对于前4条路线感觉自己解析的都比较清楚,最后一条路线似乎说的不是很清楚,后来我又查看了不少这方面的资料,对Instrumentation再次说明下。
每个Android 应用程序运行在自己的进程,Instrumentation杀死当前应用程序,并重新启动应用程序(restartsthe process with Instrumentation)。Instrumentation提供给我们一个应用程序上下文的Handle,通过这个Handle我们可以洞察应用程序,从而验证测试断言,我们还可以通过它来写一些比界面测试更加底层的测试用例。需要强调说明的是:Instrumentation不能捕获UI方面的 bugs。
Android在JUnit的基础上扩展出来的、与Instrumentation有关的3个类:
类 |
描述 |
InstrumentationTestCase |
它扩展了JUnit中的TastCase,并提供了一个接口 getInstrumentation() 获取Instrumentation类。这个可以根据自己的需求来扩展这个类,比如说:测试中可能会启动某个Avtivity和发送按键事件,为此完成测试,instrumentation需要将其注入到TastCase中。 |
InstrumentationTestRunner |
它是Instrumentation的基础上扩展的,它将自己注入到每个测试用例本身,测试用例需要分组到一个适当的InstrumentationTestRunner运行起来。 |
InstrumentationTestSuite |
它扩展了JUnit TestSuite,其主要作用是保证每个TestCase在运行前,Instrumentation能注入到TestCase中,InstrumentationTestRunner中需要使用InstrumentationTestSuite。 |
以上说明来自网页Instrumentation Testing(英文的),在这里推荐给大家阅读。
JUnit是采用测试驱动开发的方式,也就是说在开发前先写好测试代码,主要用来说明被测试的代码会被如何使用,错误处理等;然后开始写代码,并在测试代码中逐步测试这些代码,直到最后在测试代码中完全通过。
看了是否感觉有些不符合程序员的思维习惯(先写代码然后在调试),的确这也是JUnit是对程序员思维习惯的“颠覆”。在这里我自己也感觉,好像很难做到,为什么?在一匹“马”没有完全设计好前,怎么规定这匹“马”将来会如何跑?而且即使把“马”将来会如何“跑”定义好了,在实现的时候“马”被改变了怎么办?说到底还是:一个人不能同时具有2个角色,否则自己有时候就不知道当前是哪个角色!
说到这里,我就说明下,我自己对JUnit“错误”的使用方法,这也许与JUnit测试驱动开发的目的相矛盾,但是的确可以有效地减少bug。JUnit从核心来说就是将源代码与测试代码完全分开,将测试代码作为一个单独的程序。前面介绍的方法,都将源代码与测试代码合为一体,由于源代码的重要性大于测试代码的重要性,所以测试代码经常有不完整、结构不清晰等问题,这样程序员的单元测试也就不完整。JUnit就是被我用来做完整的单元测试,对当前的部分代码,测试其在每种“环境”下的运行结果。
历时半个月的学习,总算把Android、JUnit深入解析篇写完了,还是比较全面的介绍了Android中使用JUnit的方方面面,在写博客的过程中,我也尽量把自己遇到的每个问题,在这里与大家分享,并尽量把每个问题解析清楚。如果你看了之后,感觉有什么不足、错误、遗漏的地方,欢迎大家在博客中留言。