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.1 InstrumentationTestCase
當你要測試與Android環境無關的方法時,推薦繼承InstrumentationTestCase來進行測試。比如下面的比大小的方法就很適合做這樣的測試。
public static int getMax(int a, int b) { return a >= b ? a : b; }
得到版本號的代碼因為涉及到了Context所以和android運行的環境有關,我們必須要傳入一個上下文(context)對象,這時繼承InstrumentationTestCase就沒有辦法進行測試了。
/** 取得當前應用的版本號 * @param context * @return */ public static String getVersionName(Context context) { try { PackageInfo manager = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); return manager.versionName; } catch (NameNotFoundException e) { return "Unknown"; } }
那么是不是用它就不能對activity這樣的東西進行測試了呢?也不是,我們仍舊可以用它來測試Activity,前題是要通過代碼初始化對象,但因為它的子類可以針對Activity進行完善的測試,所以我們一般不用它來做測試activity的工作。第二節中,先給出了一個簡單的demo,然后給出用它測試activity的demo。
2.2 舉例
測試的目標類——MyUtils:
package com.kale.androidtest; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager.NameNotFoundException; public class MyUtils { public static int getMax(int a, int b) { return a >= b ? a : b; } }
測試類——MySimpleTest:
package com.kale.androidtest.test; import com.kale.androidtest.MyUtils; import android.test.InstrumentationTestCase; public class MySimpleTest extends InstrumentationTestCase { public void testGetMax(){ int max = MyUtils.getMax(1, 3); assertEquals(3, max); } }
附:測試Activity
activity的代碼就不貼了,里面有個editText,這里的測試也是簡單的例子,表示它可以用來測試activity,例子沒有任何實際意義。
package com.kale.androidtest.test; import android.content.Intent; import android.os.SystemClock; import android.test.InstrumentationTestCase; import android.widget.EditText; import com.kale.androidtest.MyActivity; /** * @author:Jack Tony * @description : * @web: http://www.oschina.net/question/54100_27061 * @date :2015年2月19日 */ public class MySampleTest2 extends InstrumentationTestCase { MyActivity mActivity; EditText mEditText; @Override protected void setUp() throws Exception { // 用intent啟動一個activity Intent intent = new Intent(); intent.setClassName("com.kale.androidtest", MyActivity.class.getName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mActivity = (MyActivity) getInstrumentation().startActivitySync(intent); } /** * @description 測試是否初始化完成 * */ public void testInit() { mEditText = mActivity.getEditText(); assertNotNull(mActivity); assertNotNull(mEditText); } /** * @description 測試得到activity中editText中的文字 * */ public void testGetText() { mEditText = mActivity.getEditText(); String text = mEditText.getText().toString(); assertEquals("", text); } /** * @description 測試設置文字的方法 * */ public void testSetText() { mEditText = mActivity.getEditText(); // 在主線程中設置文字 getInstrumentation().runOnMainSync(new Runnable() { @Override public void run() { mEditText.setText("kale"); } }); // 暫停1500ms SystemClock.sleep(1000); assertEquals("kale", mEditText.getText().toString()); } /** * 垃圾清理與資源回收 * * @see android.test.InstrumentationTestCase#tearDown() */ @Override protected void tearDown() { 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
package com.kale.androidtest; import android.app.Application; public class MyApplication extends Application{ @Override public void onCreate() { // TODO 自動生成的方法存根 super.onCreate(); } public String 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); * } */ // 調用父類構造函數,且構造函中傳遞的參數為被測試的類 public MyApplicationTest() { super(MyApplication.class); }
在測試的類中我們初始化了application,之后測試了application中的一個獲取字符串的方法,代碼如下:
package com.kale.androidtest.test; import android.test.ApplicationTestCase; import com.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> { private MyApplication application; /* * 注意:構造函數不能這么寫,否則會找不到類 * @web :http://www.educity.cn/wenda/164209.html * * public MyApplicationTest(Class<MyApplication> applicationClass) { * super(applicationClass); * } */ // 調用父類構造函數,且構造函中傳遞的參數為被測試的類 public MyApplicationTest() { super(MyApplication.class); } /* * 初始化application * @throws Exception */ @Override protected void setUp() throws Exception { super.setUp(); // 獲取application之前必須調用的方法 createApplication(); // 獲取待測試的FxAndroidApplication application = getApplication(); } public void testGetString() { 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的文字。
package com.kale.androidtest; public class MyActivity extends Activity { private TextView testTv; private EditText testEt; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } private void initView() { testEt = (EditText)findViewById(R.id.test_editText); testTv = (TextView)findViewById(R.id.test_textView); Button testBtn = (Button)findViewById(R.id.test_button); testBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { testTv.setText("kale"); } }); } /** * @description 得到textview中的text * * @return */ public String getText() { return testTv.getText().toString(); } /** * @description 不想修改本例中的editText的可見范圍,但有需要在測試時得到這個控件,所以就get一下 * * @return */ public EditText getEditText() { return testEt; } /** * @description 本例中的button僅僅用了一次,僅僅在initView()方法中出現 * 這里為了測試它的點擊事件,所以重新find了這個button * * @return */ public Button getButton() { return (Button)findViewById(R.id.test_button); } }
4.2 測試activity的代碼
測試activity需要繼承ActivityInstrumentationTestCase2這個類,ActivityInstrumentationTestCase已經被廢棄了,所以不用管它。這個類中提供了創建activity的方法,也提供了得到activity的方法,發送按鍵事件的方法,當然還有些別的方法。需要注意的是,類的構造函數必須是無參的,當傳遞按鍵前我們要屏蔽activity的touch事件,我個人建議不要去測試發送按鍵的事件,因為發送按鍵和當前輸入法有很大關系,而且一般情況下我們完全沒必要去測試用戶輸入不同數據的情況,直接用setText方法就好了。在測試任何view的方法前,我們都要讓其獲得焦點,然后給它一個按下的事件,這樣我們就模擬了操作。
下面是測試類的代碼,詳細的解釋都在注釋里面了。
package com.kale.androidtest.test; import android.app.Instrumentation; import android.test.ActivityInstrumentationTestCase2; import android.view.KeyEvent; import com.kale.androidtest.MyActivity; public class MyActivityTest extends ActivityInstrumentationTestCase2<MyActivity> { private Instrumentation mInstrumentation; private MyActivity mActivity; public MyActivityTest() { super(MyActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); /**這個程序中需要輸入用戶信息和密碼,也就是說需要發送key事件, * 所以,必須在調用getActivity之前,調用下面的方法來關閉 * touch模式,否則key事件會被忽略 */ //關閉touch模式 setActivityInitialTouchMode(false); mInstrumentation = getInstrumentation(); // 獲取被測試的activity mActivity = getActivity(); } /** * @description 該測試用例實現在測試其他用例之前,看edittext是否為空 * */ public void testPreConditions() { assertNotNull(mActivity.getEditText()); } /** * @description 簡單測試textview初始的字符串 * */ public void testGetText() { assertEquals("Hello world!", mActivity.getText()); } /** * @description 測試button的點擊事件,看看點擊后textview的值有沒有改變 * */ public void testClick() { // 開新線程,並通過該線程在實現在UI線程上執行操作,這里是在主線程中的操作 mInstrumentation.runOnMainSync(new Runnable() { @Override public void run() { // 得到焦點 mActivity.getButton().requestFocus(); // 模擬點擊事件 mActivity.getButton().performClick(); } }); assertEquals("kale", mActivity.getText()); } /** * 該方法實現在登錄界面上輸入相關的登錄信息。由於UI組件的 * 相關處理(如此處的請求聚焦)需要在UI線程上實現, * 所以需調用Activity的runOnUiThread方法實現。 */ public void testInput() { // 在UI線程中進行操作,讓editText獲取焦點 mActivity.runOnUiThread(new Runnable() { @Override public void run() { 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.3 ActivityUnitTestCase
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 extends Service{ @Override public IBinder onBind(Intent intent) { // TODO 自動生成的方法存根 return null; } public int sum(int a, int b) { return a + b; } }
測試代碼:
package com.kale.androidtest.test; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.test.ServiceTestCase; import android.test.suitebuilder.annotation.MediumTest; import com.kale.androidtest.MyService; /** * @author:Jack Tony * @description : * * ServiceTestCase 默認的執行testAndroidTestCaseSetupProperly()方法。用於驗證該測試類是否在跑其他測試用例之前成功地設置了上下文。 * * @date :2015年2月18日 */ public class MyServiceTest extends ServiceTestCase<MyService> { private MyService mService; public MyServiceTest() { super(MyService.class); } @Override protected void setUp() throws Exception { 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 void setupService() { super.setupService(); } @Override protected void shutdownService() { // TODO 自動生成的方法存根 super.shutdownService(); } @Override protected void tearDown() throws Exception { // TODO 自動生成的方法存根 super.tearDown(); } public void testPreConditions() { assertNotNull(mService); } public void testSum() { //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() throws Exception { // ……異步操作的回掉方法 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() throws Throwable { // 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 = new MyAsyncTask() { @Override protected void onPostExecute(String result) { super.onPostExecute(result); assertEquals("kale", result); // notify the count down latch signal.countDown(); } }; // Execute the async task on the UI thread! runTestOnUiThread(new Runnable() { @Override public void run() { // 執行異步任務 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