前文已經對基於junit的android測試架構有了一個大概的介紹,下面我們對activity測試進行分析。
本文主要舉兩個基於Robotium的activity測試例子,一個是測試單個activity,一個測試多個activity。
1、Robotium概述
首先,我們來瞭解一下android的測試類別的階層:
可以看出android中的測試方法主要有AndroidTextCase和InstrumentationTextCase。在這篇文章中,我將介紹Instrumentation這種測試方法,那麼什麼是Instrumentation?
Instrumentation和Activity有點類似,只不過Activity是需要一個介面的,而Instrumentation並不是這樣的,我們可以將它理解為一種沒有圖形介面的,具有啟動能力的,用於監控其他類(用Target Package聲明)的工具類。
2、單個activity測試例子
2.1普通測試
我想大家在安裝完robotium後,都會試試noteslist 這個例子吧。這個是官網提到的例子
首先開啟noteslist 源碼
\samples\android-7\NotePad
再開啟noteslisttest 源碼
可以從上面下載 http://code.google.com/p/robotium/downloads/list/ExampleTestProject_v2.3.zip
要做一點修改。 因為noteslist是在androidV21開發的,而我的測試代碼是V23的。我們最好要改成一致的。
修改 noteslisttest 下的AndroidManifest.xml
<uses-sdk android:minSdkVersion="9" />
改成<uses-sdk android:minSdkVersion="7" />
這兩個數字表示什麼意思呢?
7--androidV21,9--androidV23,最低版本是3--AndroidV15.
大家按順序排就知道哪個數字對應的版本了
然後在 noteslisttest 右擊選中Properties--Android,選中AndroidV21
這樣noteslisttest 裡帶的android jar 由android2.3 變為android2.1
再說一個配置,我覺得也很重要
還是在AndroidManifest.xml 裡
<instrumentation android:targetPackage="com.example.android.notepad" android:name="android.test.InstrumentationTestRunner" />
紅色加粗的字串表示我們要測試代碼的package
OK,這樣我們就弄好代碼了。 我們只需要執行Run As--Android Junit test
下面我們看看 noteslisttest 裡的具體代碼,看看它是怎麼測試的
private Solo solo;// 告知系統我要測試的app是什麼public NotePadTest() {super("com.example.android.notepad", NotesList.class);}//開啟noteslist public void setUp() throws Exception { solo = new Solo(getInstrumentation(), getActivity()); } @Smoke public void testAddNote() throws Exception { solo.clickOnMenuItem("Add note"); solo.assertCurrentActivity("Expected NoteEditor activity", "NoteEditor"); //Assert that NoteEditor activity is opened solo.enterText(0, "Note 1"); //In text field 0, add Note 1 solo.goBack(); //Go back solo.clickOnMenuItem("Add note"); //Clicks on menu item solo.enterText(0, "Note 2"); //In text field 0, add Note 2 solo.goBackToActivity("NotesList"); //Go back to first activity named "NotesList" boolean expected = true; boolean actual = solo.searchText("Note 1") && solo.searchText("Note 2"); assertEquals("Note 1 and/or Note 2 are not found", expected, actual); //Assert that Note 1 & Note 2 are found }
這是我們第一個case,主要目的是測試添加文本的功能
clickOnMenuItem(String)
功能是點擊Menu按鈕,選擇文本描述為String的菜單,如我們的例子是"Add note"
assertCurrentActivity(String message,String name)
這個是判斷當前的activity是否和我預期的一致
message是描述性的文字
name是指activity的名字
關於如何知道activity 名字,我找了半天的文檔,目前的方法是得看源碼中的 AndroidManifest.xml--Application label--Application Nodes,在那裡我們可以看到所有的activity的name
enterText(int index,string text)
index用來標識寫到哪個EditText中。如果當前只開啟一個EditText,那index=0
text:就是我們要寫入的內容
goBack()
相當於手機上的 返回鍵(back key)
goBackToActivity(String name)
返回到指定的activity
searchText(String text)
在當前的activity中搜尋是否含有text的內容
@Smoke public void testEditNote() throws Exception {solo.clickInList(2); // Clicks on the second list linesolo.setActivityOrientation(Solo.LANDSCAPE); // Change orientation of activitysolo.clickOnMenuItem("Edit title"); // Change titlesolo.enterText(0, " test"); //In first text field (0), add test. solo.goBackToActivity("NotesList");boolean expected = true;boolean actual = solo.searchText("(?i).*?note 1 test"); // (Regexp) case insensitive// insensitiveassertEquals("Note 1 test is not found", expected, actual); //Assert that Note 1 test is found}
第二個case,主要是測試編輯功能的
clickInList(int index)
點擊list表的第index行,進入該文本介面
solo.setActivityOrientation(Solo.LANDSCAPE);
setActivityOrientation,設定手機螢幕顯示方式
LANDSCAPE:橫向顯示
Portrait:豎向顯示
@Smoke public void testRemoveNote() throws Exception { solo.clickOnText("(?i).*?test.*"); //(Regexp) case insensitive/text that contains "test" solo.clickOnMenuItem("Delete"); //Delete Note 1 test boolean expected = false; //Note 1 test & Note 2 should not be found boolean actual = solo.searchText("Note 1 test"); assertEquals("Note 1 Test is found", expected, actual); //Assert that Note 1 test is not found solo.clickLongOnText("Note 2"); solo.clickOnText("(?i).*?Delete.*"); //Clicks on Delete in the context menu actual = solo.searchText("Note 2"); assertEquals("Note 2 is found", expected, actual); //Assert that Note 2 is not found }
第三個case,是用來測試刪除功能的
clickOnText(String text)
點擊包含該文字的地方
其中text可以用Regex表示
(?i)----忽略大小寫。預設情況是大小寫敏感的。
Regex與java保持一致
clickLongOnText(String text)
長時間按住所選的文字
這裡需要注意:被測apk和測試apk必須使用相同的簽名。
2.2 資料驅動測試
本例與上一例子都是對於單個activity測試,不同的地方在於本例使用的測試資料來源於檔案。
被測試代碼是簡易計算機,代碼: /Files/morebetter/android
code/AndroidCalculator.rar
1. 資料驅動測試架構
測試資料來源:TestData.csv
First Value |
Second Value |
10 |
1.5 |
20 |
3 |
第一個輸入框從First Value中讀資料
第二個輸入框從Second Value中讀資料
點擊Multiply
比較測試結果和期望結果是否一致,將結果寫到檔案裡
2. 建立資料來源檔案
格式如
3. 把資料來源檔案上傳到Emulator上
在被測試代碼中建立res/raw/files檔案夾。這樣files檔案夾就能被上傳到Emulator上了
用Eclipse—Run As—Android Application 運行被測試代碼
在Eclipse上載入DDMS,點擊File Exploer,瀏覽Emulator-5554的所有檔案
開啟/data/data/com.calculator/files, 點擊右側上傳到device的按鈕,將csv檔案上傳到emulator上
4. 編輯測試case, 代碼為:/Files/morebetter/android code/AndroidCalculatorTestApk.rar
5. 運行測試case
6. 將測試結果寫到檔案裡,該檔案存放在/data/data/com.calculator/files 下面
7. 將測試結果匯入到本地電腦中
3、多個activity測試
在Android SDK中“Resources”-“Tutorials”下有“Notepad Tutorial”和“Activity Testing”兩個項目,一個樣本是指導你如何快速開發一個Android小程式,一個是指導你如何對項目進行測試,兩個項目都適合在入門的時候好好學習。
其中的“Activity Testing”是對“Samples”-“Spinner”項目進行測試,其中包含了UI測試、狀態破壞和狀態恢複測試。這個項目只有一個Activity,測試起來也不麻煩,細心閱讀文檔就可以完成。但是一個程式只有一個Activity應該是很難遇見的吧,那麼應該對多活動(Multi Activities)的程式進行測試呢?
其實我這也是隨便整整,大家隨便看看。
在查看SDK關於測試的章節後,有疑問如下:
測試Activity、Service、Provider都是自動化的,那麼我們如何控制運行過程?
如何在介面類比操作,如點擊按鈕,輸入文字內容等等。
建立一個項目,項目名為Login,包名為com.vruc.android.login,程式名為Login,活動名為AuthenticateActivity;同時添加一個項目名為LoginTest,包名為com.vruc.android.login.test,程式名為LoginTest的測試專案。
完整的Login項目:
1.更改main.xml檔案名稱為login.xml,更改代碼為下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <EditText android:id="@+id/username_field" android:layout_height="wrap_content" android:layout_width="match_parent"></EditText> <EditText android:id="@+id/password_field" android:layout_height="wrap_content" android:layout_width="match_parent"></EditText> <Button android:text="Login" android:id="@+id/login_button" android:layout_width="wrap_content" android:layout_height="wrap_content"></Button> </LinearLayout>
2.開啟AuthenticateActivity.java檔案,為“@+id/login_button”添加點擊事件,具體代碼就是向WelcomeActivity傳遞當前“@+id/username_field”中的輸入文字並結束當前activity,具體代碼為下:
public class AuthenticateActivity extends Activity {/** Called when the activity is first created. */@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.login);Button login = (Button) findViewById(R.id.login_button);login.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {Intent i = new Intent(AuthenticateActivity.this,WelcomeActivity.class);i.putExtra(ACCOUNT_SERVICE,((EditText) findViewById(R.id.username_field)).getText().toString());startActivity(i);finish();}});}}
3.在layout目錄下添加新檔案welcome.xml,更改代碼為下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="fill_parent"android:layout_height="fill_parent"><TextView android:id="@+id/welcome_message"android:layout_width="wrap_content" android:layout_height="wrap_content"android:textSize="15pt"></TextView></LinearLayout>
4.添加新的WelcomeActivity.java檔案並在AndroidMainifest.xml中註冊,重寫onCreate事件,具體代碼就是為“@+id/welcome_message”賦值,從LoginActivity中傳遞過來的“@+id/username_field”的值,具體代碼為下:
public class WelcomeActivity extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.welcome);Intent i = this.getIntent();((TextView)findViewById(R.id.welcome_message)).setText(i.getStringExtra(ACCOUNT_SERVICE));}
現在可以運行一下Login項目,可以發現填寫在“@+id/username_field”中的文字在點擊“@+id/login_button”按鈕後出現在了WelcomeActivity中。
完整的LoginTest項目
1.添加LoginTest.java檔案,繼承類為android.test.InstrumentationTestCase
2.完整LoginTest.java中測試代碼:
public static final String TEST_USERNAME = "TEST_USERNAME";public static final String TEST_PASSWORD = "TEST_PASSWORD";public void testUserLogin() {// 註冊最開始的活動並運行Instrumentation instrumentation = getInstrumentation();ActivityMonitor monitor = instrumentation.addMonitor(AuthenticateActivity.class.getName(), null, false);// 運行活動Intent intent = new Intent(Intent.ACTION_MAIN);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);intent.setClassName(instrumentation.getTargetContext(), AuthenticateActivity.class.getName());instrumentation.startActivitySync(intent);// 等待Authenticate活動開始Activity currentActivity = getInstrumentation().waitForMonitorWithTimeout(monitor, 5);assertTrue(currentActivity != null);// 自動輸入預定的使用者名稱View currentView = currentActivity.findViewById(com.vruc.android.login.R.id.username_field);assertTrue(currentView != null);TouchUtils.clickView(this, currentView);instrumentation.sendStringSync(TEST_USERNAME);// 自動輸入預定的密碼currentView = currentActivity.findViewById(com.vruc.android.login.R.id.password_field);assertTrue(currentView != null);TouchUtils.clickView(this, currentView);instrumentation.sendStringSync(TEST_PASSWORD);// 移除當前活動監視,註冊新的活動監視,要在還沒有按下按鈕前準備instrumentation.removeMonitor(monitor);monitor = instrumentation.addMonitor(WelcomeActivity.class.getName(), null, false);// 自動點擊登陸按鈕currentView = currentActivity.findViewById(com.vruc.android.login.R.id.login_button);assertTrue(currentView != null);TouchUtils.clickView(this, currentView);// 等待Welcome活動開始currentActivity = getInstrumentation().waitForMonitorWithTimeout(monitor, 5);currentView = currentActivity.findViewById(com.vruc.android.login.R.id.welcome_message);assertTrue(currentView != null);assertEquals(TEST_USERNAME, ((TextView) currentView).getText().toString());}
運行測試程式後可以發現testUserLogin順利通過,也可以在模擬器查看具體的運行過程,就像你新手操作模擬器一般。
4、Activity 啟動 Instrumentation 測試
和startActivity 及 startService類似
在activity中 啟動Instrumentation 以便調用運行測試專案 ActivityInstrumentationTestCase2
可以嘗試如下代碼實現
startInstrumentation(new ComponentName("com.example.test", "android.test.InstrumentationTestRunner"), null, null);
5、從Intent中擷取資料
大多數Activity在啟動時,都會從Intent中擷取一些資料。
在使用Robotium測試時,當然也會需要從Activity中擷取資料。
可用的流程為
1。將setUp()方法中的
solo = new Solo(getInstrumentation(), getActivity());
轉移到每一個testXXX方法中。
2.在該語句前,可以做Intent的注入,例如
Intent intent=new Intent(); Bundle b=new Bundle(); b.putParcelable(Account.class.getName(), account); b.putParcelable(User.class.getName(), user); intent.putExtras(b); setActivityIntent(intent);
3.需要注意的是,需要將所有有關Activity的操作,放在
solo = new Solo(getInstrumentation(), getActivity());之後,例如
有操作本地Key-Value儲存的,需要早solo= 之後執行。否則會引起Activity提前執行個體化。導致Intent注入失敗
參考文獻:
android單元測試初探——Instrumentation
Activity 啟動 Instrumentation 測試
學習NotesList(Robotium內建的例子)
Android Test - Auto Test Multi Activities
Robotium 資料驅動測試架構