Android提供了上面的多个测试类,可以允许我们对于单个方法、Activity、Service、Application等多个对象进行测试,单元测试可以很方便的让我们对代码进行测试,并且方便对重构后的代码进行检查。本篇将简要的讲解如何对Android中的对象进行测试。
一、准备工作
首先在manifest.xml中添加权限和相关配置代码。
在Application外添加:
<uses-permission android:name="android.permission.RUN_INSTRUMENTATION" /> <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.kale.androidtest" />
com.kale.androidtest是包名,意思是被测类所在的包名。
在Application中添加:
<uses-library android:name="android.test.runner" />
配置文件代码一览:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.kale.androidtest" android:versionCode="1" android:versionName="1.0" > <uses-permission android:name="android.permission.RUN_INSTRUMENTATION" /> <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="com.kale.androidtest" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="21" /> <application android:name="com.kale.androidtest.MyApplication" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <uses-library android:name="android.test.runner" /> <activity android:name=".MyActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name="com.kale.androidtest.MyService"/> </application> </manifest>
注意:下面写的测试的方法一定要是public的,不然会报错!
二、测试与Android运行环境无关的方法
2.1InstrumentationTestCase
当你要测试与Android环境无关的方法时,推荐继承InstrumentationTestCase来进行测试。比如下面的比大小的方法就很适合做这样的测试。
public static int getMax(int a, intb) { return a >= b ?a : b; }
得到版本号的代码因为涉及到了Context所以和android运行的环境有关,我们必须要传入一个上下文(context)对象,这时继承InstrumentationTestCase就没有办法进行测试了。
/**取得当前应用的版本号 * @paramcontext * @return */ public staticString getVersionName(Context context) { try{ PackageInfo manager = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); returnmanager.versionName; } catch(NameNotFoundException e) { return "Unknown"; } }
那么是不是用它就不能对activity这样的东西进行测试了呢?也不是,我们仍旧可以用它来测试Activity,前题是要通过代码初始化对象,但因为它的子类可以针对Activity进行完善的测试,所以我们一般不用它来做测试activity的工作。第二节中,先给出了一个简单的demo,然后给出用它测试activity的demo。
2.2 举例
测试的目标类——MyUtils:
packagecom.kale.androidtest; importandroid.content.Context; importandroid.content.pm.PackageInfo; importandroid.content.pm.PackageManager.NameNotFoundException; public classMyUtils { public static int getMax(int a, intb) { return a >= b ?a : b; } }
测试类——MySimpleTest:
packagecom.kale.androidtest.test; importcom.kale.androidtest.MyUtils; importandroid.test.InstrumentationTestCase; public class MySimpleTest extendsInstrumentationTestCase { public voidtestGetMax(){ int max = MyUtils.getMax(1, 3); assertEquals(3, max); } }
附:测试Activity
activity的代码就不贴了,里面有个editText,这里的测试也是简单的例子,表示它可以用来测试activity,例子没有任何实际意义。
packagecom.kale.androidtest.test; importandroid.content.Intent; importandroid.os.SystemClock; importandroid.test.InstrumentationTestCase; importandroid.widget.EditText; importcom.kale.androidtest.MyActivity; /** * @author:Jack Tony * @description : * @web: http://www.oschina.net/question/54100_27061 * @date :2015年2月19日 */ public class MySampleTest2 extendsInstrumentationTestCase { MyActivity mActivity; EditText mEditText; @Override protected void setUp() throwsException { //用intent启动一个activity Intent intent = newIntent(); intent.setClassName("com.kale.androidtest", MyActivity.class.getName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mActivity =(MyActivity) getInstrumentation().startActivitySync(intent); } /** * @description 测试是否初始化完成 * */ public voidtestInit() { mEditText =mActivity.getEditText(); assertNotNull(mActivity); assertNotNull(mEditText); } /** * @description 测试得到activity中editText中的文字 * */ public voidtestGetText() { mEditText =mActivity.getEditText(); String text =mEditText.getText().toString(); assertEquals("", text); } /** * @description 测试设置文字的方法 * */ public voidtestSetText() { mEditText =mActivity.getEditText(); //在主线程中设置文字 getInstrumentation().runOnMainSync(newRunnable() { @Override public voidrun() { mEditText.setText("kale"); } }); //暂停1500ms SystemClock.sleep(1000); assertEquals("kale", mEditText.getText().toString()); } /** * 垃圾清理与资源回收 * * @seeandroid.test.InstrumentationTestCase#tearDown() */ @Override protected voidtearDown() { mActivity.finish(); try{ super.tearDown(); } catch(Exception e) { e.printStackTrace(); } } }
三、测试Application
3.1 ApplicationTestCase
首先我们来看看如何测试一个application,application是全局的一个对象,android提供了一个ApplicationTestCase来测试application。继承这个类后可以调用它自身的方法来构造和初始化一个application,得到的这个application就是我们要测试的application,接着我们就能测试它的公有方法了。这里注意,测试的代码和application的代码运行在不同的线程中,所以如果涉及到必须主线程才能进行的操作,比如更新UI等,就需要把代码传递到主线程中进行测试了。
3.2 待测试的Application
packagecom.kale.androidtest; importandroid.app.Application; public class MyApplication extendsApplication{ @Override public voidonCreate() { //TODO 自动生成的方法存根 super.onCreate(); } publicString getTestString() { return "kale"; } }
3.3 测试代码
测试的代码中有两个重要的方法:createApplication()和getApplication(),通过建立一个application的方法来初始化application,通过得到application的方法来获得要测试的application。
注意:测试类的构造方法,必须是无参数的
/* * 注意:构造函数不能这么写,否则会找不到类 * @web :http://www.educity.cn/wenda/164209.html * * public MyApplicationTest(Class<MyApplication> applicationClass) { * super(applicationClass); * } */ //调用父类构造函数,且构造函中传递的参数为被测试的类 publicMyApplicationTest() { super(MyApplication.class); }
在测试的类中我们初始化了application,之后测试了application中的一个获取字符串的方法,代码如下:
packagecom.kale.androidtest.test; importandroid.test.ApplicationTestCase; importcom.kale.androidtest.MyApplication; /** * @author:Jack Tony * @description : * @web: http://blog.csdn.net/stevenhu_223/article/details/8298858 * @date :2015年2月19日 */ public class MyApplicationTest extends ApplicationTestCase<MyApplication>{ privateMyApplication application; /* * 注意:构造函数不能这么写,否则会找不到类 * @web :http://www.educity.cn/wenda/164209.html * * public MyApplicationTest(Class<MyApplication> applicationClass) { * super(applicationClass); * } */ //调用父类构造函数,且构造函中传递的参数为被测试的类 publicMyApplicationTest() { super(MyApplication.class); } /* * 初始化application * @throws Exception */ @Override protected void setUp() throwsException { super.setUp(); //获取application之前必须调用的方法 createApplication(); //获取待测试的FxAndroidApplication application =getApplication(); } public voidtestGetString() { String realStr =application.getTestString(); assertEquals("kale", realStr); } }
四、测试Activity
4.1 要测试的activity
activity的布局文件很简单,有textview,edittext,button组成,相互独立,看效果就知道了。
布局文件的代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="${relativePackage}.${activityClass}" > <TextView android:id="@+id/test_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="@string/hello_world" /> <Button android:id="@+id/test_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_marginBottom="90dp" android:text="Button" /> <EditText android:id="@+id/test_editText" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_above="@+id/test_textView" android:layout_centerHorizontal="true" android:layout_marginBottom="79dp" android:ems="10" > <requestFocus /> </EditText> </RelativeLayout>
Activity代码
activity在onCreat()中初始化了控件,给button添加了一个监听器,button按下后设置textview的文字。
packagecom.kale.androidtest; public class MyActivity extendsActivity { privateTextView testTv; privateEditText testEt; @Override protected voidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private voidinitView() { testEt =(EditText)findViewById(R.id.test_editText); testTv =(TextView)findViewById(R.id.test_textView); Button testBtn =(Button)findViewById(R.id.test_button); testBtn.setOnClickListener(newOnClickListener() { @Override public voidonClick(View v) { testTv.setText("kale"); } }); } /** * @description 得到textview中的text * * @return */ publicString getText() { returntestTv.getText().toString(); } /** * @description 不想修改本例中的editText的可见范围,但有需要在测试时得到这个控件,所以就get一下 * * @return */ publicEditText getEditText() { returntestEt; } /** * @description 本例中的button仅仅用了一次,仅仅在initView()方法中出现 * 这里为了测试它的点击事件,所以重新find了这个button * * @return */ publicButton getButton() { return(Button)findViewById(R.id.test_button); } }
4.2 测试activity的代码
测试activity需要继承ActivityInstrumentationTestCase2这个类,ActivityInstrumentationTestCase已经被废弃了,所以不用管它。这个类中提供了创建activity的方法,也提供了得到activity的方法,发送按键事件的方法,当然还有些别的方法。需要注意的是,类的构造函数必须是无参的,当传递按键前我们要屏蔽activity的touch事件,我个人建议不要去测试发送按键的事件,因为发送按键和当前输入法有很大关系,而且一般情况下我们完全没必要去测试用户输入不同数据的情况,直接用setText方法就好了。在测试任何view的方法前,我们都要让其获得焦点,然后给它一个按下的事件,这样我们就模拟了操作。
下面是测试类的代码,详细的解释都在注释里面了。
packagecom.kale.androidtest.test; importandroid.app.Instrumentation; importandroid.test.ActivityInstrumentationTestCase2; importandroid.view.KeyEvent; importcom.kale.androidtest.MyActivity; public class MyActivityTest extends ActivityInstrumentationTestCase2<MyActivity>{ privateInstrumentation mInstrumentation; privateMyActivity mActivity; publicMyActivityTest() { super(MyActivity.class); } @Override protected void setUp() throwsException { super.setUp(); /**这个程序中需要输入用户信息和密码,也就是说需要发送key事件, * 所以,必须在调用getActivity之前,调用下面的方法来关闭 * touch模式,否则key事件会被忽略 */ //关闭touch模式 setActivityInitialTouchMode(false); mInstrumentation =getInstrumentation(); //获取被测试的activity mActivity =getActivity(); } /** * @description 该测试用例实现在测试其他用例之前,看edittext是否为空 * */ public voidtestPreConditions() { assertNotNull(mActivity.getEditText()); } /** * @description 简单测试textview初始的字符串 * */ public voidtestGetText() { assertEquals("Hello world!", mActivity.getText()); } /** * @description 测试button的点击事件,看看点击后textview的值有没有改变 * */ public voidtestClick() { //开新线程,并通过该线程在实现在UI线程上执行操作,这里是在主线程中的操作 mInstrumentation.runOnMainSync(newRunnable() { @Override public voidrun() { //得到焦点 mActivity.getButton().requestFocus(); //模拟点击事件 mActivity.getButton().performClick(); } }); assertEquals("kale", mActivity.getText()); } /** * 该方法实现在登录界面上输入相关的登录信息。由于UI组件的 * 相关处理(如此处的请求聚焦)需要在UI线程上实现, * 所以需调用Activity的runOnUiThread方法实现。 */ public voidtestInput() { //在UI线程中进行操作,让editText获取焦点 mActivity.runOnUiThread(newRunnable() { @Override public voidrun() { mActivity.getEditText().requestFocus(); mActivity.getEditText().performClick(); } }); /* * 由于测试用例在单独的线程上执行,所以此处需要同步application, * 调用waitForIdleSync等待测试线程和UI线程同步,才能进行输入操作。 * waitForIdleSync和sendKeys不允许在UI线程里运行 */ mInstrumentation.waitForIdleSync(); //调用sendKeys方法,输入字符传。这里输入的是TEST123 sendKeys(KeyEvent.KEYCODE_SHIFT_LEFT,KeyEvent.KEYCODE_T, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_T, KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_3); assertEquals("test123", mActivity.getEditText().toString()); } }
4.3ActivityUnitTestCase
ActivityUnitTestCase也是activity的单元测试类,而且在理论上说它才是真正的activity单元测试。运行它的测试类后不会产生一个activity的界面,它会用底层来做处理,正因为如此,它里面不能有数据存储和交互依赖关系。个人了解后发现用它仅仅能测试下按键事件,或者是得到启动当前activity的intent、code等,还可以得到activity传递的code。调用下面的方法时会抛出异常信息,不应该去使用。
createPendingResult(int, Intent, int) startActivityIfNeeded(Intent, int) startActivityFromChild(Activity, Intent, int) startNextMatchingActivity(Intent) getCallingActivity() getCallingPackage() createPendingResult(int, Intent, int) getTaskId() isTaskRoot() moveTaskToBack(boolean)
下面的方法可以调用,但一般不起任何作用,你可以使用getStartedActivityIntent()和getStartedActivityRequest() 来检查参数值。
startActivity(Intent) startActivityForResult(Intent, int)
下面的方法也可以调用,一般也无效果,可以使用isFinishCalled() 和getFinishedActivityRequest检查传入的参数。
finish() finishFromChild(Activity) finishActivity(int)
我个人认为当你测试activity的intent或者传递的code可以用这个类,否则直接用ActivityInstrumentationTestCase2就好了,如果想要继续了解ActivityUnitTestCase,请参考:
http://blog.csdn.net/mapdigit/article/details/7589430
http://myeyeofjava.iteye.com/blog/1972435
五、测试Service
本段文字参考自:http://blog.csdn.net/yan8024/article/details/6271715
由于Service是在后台运行的,所以测试Service不能用instrumentation框架,继承ServiceTestCase的测试类可以对service进行针对性测试。ServiceTestcase不会初始化测试环境直到你调用ServiceTestCase.startService()或者ServiceTestCase.bindService. 这样的话,你就可以在Service启动之前可以设置测试环境,创建你需要模拟的对象等等。比如你可以配置service的context和application对象。
setApplication()方法和setContext(Context)方法允许你在Service启动之前设置模拟的Context 和模拟的Application.关于这些模拟的对象。
需要注意的是ServiceTestCase .bindService() 方法和Service.bindService()方法的参数不同的。ServiceTestCase.bindService() 方法只提供了以个intent对象。返回值方面ServiceTestCase.bindService()方法返回的是一个IBinder对象的子类, 而Service.bindService ()返回的是布尔值。
ServiceTestCase 默认的执行testAndroidTestCaseSetupProperly()方法。用于验证该测试类是否在跑其他测试用例之前成功地设置了上下文。
例子:
待测service
public class MyService extendsService{ @Override publicIBinder onBind(Intent intent) { //TODO 自动生成的方法存根 return null; } public int sum(int a, intb) { return a +b; } }
测试代码:
packagecom.kale.androidtest.test; importandroid.app.Service; importandroid.content.Intent; importandroid.os.IBinder; importandroid.test.ServiceTestCase; importandroid.test.suitebuilder.annotation.MediumTest; importcom.kale.androidtest.MyService; /** * @author:Jack Tony * @description : * * ServiceTestCase 默认的执行testAndroidTestCaseSetupProperly()方法。用于验证该测试类是否在跑其他测试用例之前成功地设置了上下文。 * * @date :2015年2月18日 */ public class MyServiceTest extends ServiceTestCase<MyService>{ privateMyService mService; publicMyServiceTest() { super(MyService.class); } @Override protected void setUp() throwsException { super.setUp(); getSystemContext();//A normal system context. //Sets the application that is used during the test. If you do not call this method, a new MockApplication object is used. setApplication(getApplication()); startService(new Intent(getContext(), MyService.class)); //启动后才能得到一个service对象,如果测试时出现空指针异常,很可能是这里没有进行初始化。以防万一你可以在测试方法第一句用getService得到service对象 mService =getService(); } @Override protected voidsetupService() { super.setupService(); } @Override protected voidshutdownService() { //TODO 自动生成的方法存根 super.shutdownService(); } @Override protected void tearDown() throwsException { //TODO 自动生成的方法存根 super.tearDown(); } public voidtestPreConditions() { assertNotNull(mService); } public voidtestSum() { //mService = getService(); int sum = mService.sum(1, 2); assertEquals(3, sum); } }
ServiceTestCase 方法说明:
getApplication() 返回被测试的Service所用的Application.
getSystemContext() 返回在setUp()方法中被保存的真的系统Context.
setApplication (Applicaition application) 设置测试被测试Service 所用的Application.
setUp() 得到当前系统的上下文并存储它。若要重写该方法的话,第一句必须是
super.setUp() 该方法在每个测试方法被执行前都执行一遍。
setupService() 生成被测试的Service , 并向其中注入模拟的组件(Appliocation ,Context)。该方法会被StartService(Intent )或者bindService(Intent)自动调用。
shutdownService() 调用相应的方法停止或者解除Service,然后调用ondestory(),通常该方法会被teardown()方法调用。
startService(Intent intent) 启动被测试的Service.如果用这个方法启动一个服务,那么该服务在最后回自动被teardown()方法关掉。
tearDown() 关闭被测试的服务, 确认在执行下个用例前所有的资源被释放,所以的垃圾被回收。 这个方法在每个方法执行完后调用。重写该方法上的话, 必须将super.tearDown()作为最后一条语句。
六、测试异步操作
异步任务可能是在activity中开始的,也可能是在service中开始的,运行环境根据实际情况而定,所以继承的测试类也不同。还得具体问题,具体分析。
6.1 思路
在测试方法中要将线程先wait,然后执行完成后调用notify去操作。如果是执行异步的操作,在测试方法中要将线程先wait,然后执行完成后调用notify去操作,比如:
private final Integer LOCK = 1; public void test() throwsException { //……异步操作的回掉方法 synchronized(LOCK) { LOCK.notify(); } try{ synchronized(LOCK) { LOCK.wait(); } } catch(InterruptedException e) { Assert.assertNotNull(e); } }
上面的代码我没理解是什么意思,来自文章:http://blog.csdn.net/henry121212/article/details/7837074
6.2 例子
这个例子中我测试asyncTask,测试的代码来自stackoverflow,在百度上没有搜到任何有用的信息。下面的代码中我测试了asyncTask执行的结果。要点是重写onPostExecute()方法,在该方法中进行结果的判断,在最后调用countDown()来释放等待锁。代码执行时在UI线程中开启异步任务,然后执行等待命令,在异步任务的最后进行判断结果并停止等待。
public void testLoginAsync2() throwsThrowable { //create a signal to let us know when our task is done. final CountDownLatch signal = new CountDownLatch(1); /* * Just create an in line implementation of an asynctask. Note this * would normally not be done, and is just here for completeness. You * would just use the task you want to unit test in your project. */ final MyAsyncTask task = newMyAsyncTask() { @Override protected voidonPostExecute(String result) { super.onPostExecute(result); assertEquals("kale", result); //notify the count down latch signal.countDown(); } }; //Execute the async task on the UI thread! runTestOnUiThread(newRunnable() { @Override public voidrun() { //执行异步任务 task.execute(); } }); signal.await(); //signal.await(30, TimeUnit.SECONDS); }
源码下载:http://download.csdn.net/detail/shark0017/8451777
参考自:
http://blog.csdn.net/yan8024/article/details/6271715
http://blog.csdn.net/stevenhu_223/article/details/8298858
http://blog.csdn.net/henry121212/article/details/7837074
http://myeyeofjava.iteye.com/blog/1972435
http://blog.csdn.net/mapdigit/article/details/7589430