Android單元測試


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>
View Code

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM