手把手教你Android標準App的四大自動化測試法寶

作者:Ringoyan,騰訊測試開發工程師。先後為植物大戰殭屍Online,糖果傳奇等遊戲擔任測試經理,其負責的“我叫MT2”測試專案曾獲騰訊互動娛樂精品文化獎銀獎。目前擔任騰訊WeTest測試經理。擅長領域:App的自動化測試和Web的安全測試工作。

注:核心內容轉自許奔的《深入理解Android自動化測試》,本書將在許奔公眾號“巴哥奔”中全文連載。

商業轉載請聯絡騰訊WeTest獲得授權,非商業轉載請註明出處。

WeTest導讀

說起Android的自動化測試,相信有很多小夥伴都接觸過或者有所耳聞,本文從框架最基本的功能介紹及API的使用入手,結合簡單的專案實戰來幫忙大家對該框架進一步理解和加深印象。下面讓我們來一睹標準App的四大自動化測試法寶的風采!

法寶1:穩定性測試利器——Monkey

要想釋出一個新版本,得先通過穩定性測試。理想情況是找個上幼兒園的弟弟妹妹,開啟應用把手機交給他,讓他胡亂的玩,看你的程式能不能接受這樣的折騰。但是我們身邊不可能都有正太和蘿莉,也不能保證他們拿到手機後不是測試軟體的健壯性,反而測試你的手機經不經摔,這與我們的期望差太遠了…
Google公司考慮到我們的需要,開發出了Monkey這個工具。但在很多人的印象中,Monkey測試就是讓裝置隨機的亂點,事件都是隨機產生的,不帶任何人的主觀性。很少有人知道,其實Monkey也可以用來做簡單的自動化測試工作。
Mokey基本功能介紹
首先,介紹下Monkey的基本使用,如果要傳送500個隨機事件,只需執行如下命令:
adb shell monkey 500

插上手機執行後,大家是不是發現手機開始瘋狂的執行起來了。So Easy!
在感受完Monkey的效果後,發現這“悟空”太調皮了,根本招架不住啊!是否有類似“緊箍咒”這種約束類命令,讓這隻猴子在某個包或類中執行呢?要想Monkey牢牢的限制在某個包中,命令也很簡單:
adb shell monkey –p your-package-name 500

-p後面接你程式的包名。多想限制在多個包中,可以在命令列中新增多個包:
adb shell monkey –p your-package1-name –p your-package2-name 500

這樣“悟空”就飛不出你的五指山了。
Mokey編寫自動化測試指令碼
若控制不住“悟空”,只讓它隨機亂點的話,Monkey是替代不了黑盒測試用例的。我們能不能想些辦法,控制住“悟空”讓他做些簡單的自動化測試的工作呢?下面來看一下,如何用Monkey來編寫指令碼。
先簡單介紹下Monkey的API,若有需要詳細瞭解的小夥伴,可自行百度或谷歌一下查閱哈。
(1) 軌跡球事件:DispatchTrackball(引數1~引數12)
(2) 輸入字串事件:DispatchString(String text)
(3) 點選事件:DispatchPointer(引數1~引數12)
(4) 啟動應用:LaunchActivity(String pkg_name, String class_name)
(5) 等待事件:UserWait(long sleeptime)
(6) 按下鍵值:DispatchPress(int keyCode)
(7) 長按鍵值:LongPress(int keyCode)
(8) 傳送鍵值:DispatchKey(引數1~引數8)
(9) 開啟軟鍵盤:DispatchFlip(Boolean keyboardOpen)
瞭解完常用API後,我們來看一下Monkey指令碼的編寫規範。Monkey Script是按照一定的語法規則編寫的有序的使用者事件流,使用於Monkey命令工具的指令碼。Monkey指令碼一般以如下4條語句開頭:

# Start Script
type = user    #指明指令碼型別
count = 10     #指令碼執行次數
speed = 1.0    #命令執行速率
start data >>  #使用者指令碼入口,下面是使用者自己編寫的指令碼

下面來看一個簡單應用的實戰,實現的效果很簡單,就是隨便輸入文字,選擇選項再進行提交,提交後要驗證提交後的效果。
這裡寫圖片描述
這裡寫圖片描述

# Start
Script
type = user
count = 10
speed = 1.0
start data >>LaunchActivity(com.ringo.bugben,com.ringo.bugben.MainActivity)
# 點選文字框1
captureDispatchPointer(10,10,0,210,200,1,1,-1,1,1,0,0)
captureDispatchPointer(10,10,1,210,200,1,1,-1,1,1,0,0)
# 確定文字框1內容
captureDispatchString(Hello)
# 點選文字框2
captureDispatchPointer(10,10,0,210,280,1,1,-1,1,1,0,0)
captureDispatchPointer(10,10,1,210,280,1,1,-1,1,1,0,0)
# 確定文字框2內容
captureDispatchString(Ringo)
# 點選加粗
captureDispatchPointer(10,10,0,210,420,1,1,-1,1,1,0,0)
captureDispatchPointer(10,10,1,210,420,1,1,-1,1,1,0,0)
# 點選大號
captureDispatchPointer(10,10,0,338,476,1,1,-1,1,1,0,0)
captureDispatchPointer(10,10,1,338,476,1,1,-1,1,1,0,0)
# 等待500毫秒
UserWait(500)
# 點選提交
captureDispatchPointer(10,10,0,100,540,1,1,-1,1,1,0,0)
captureDispatchPointer(10,10,1,100,540,1,1,-1,1,1,0,0)

將上述程式碼另存為HelloMonkey檔案,然後將該指令碼推送到手機的sd卡里。

adb push HelloMonkey /mnt/sdcard/

然後執行:

adb shell monkey -v -f /mnt/sdcard/HelloMonkey 1

指令碼後面的數字1表示執行該指令碼的次數。小夥伴們可以安裝附件裡的Bugben.apk再執行下指令碼感受下哦!

Monkey工具總結

Monkey可以編寫指令碼做簡單的自動化測試,但侷限性非常大,例如無法進行截圖操作,不能簡單的支援外掛的編寫,沒有好的辦法控制事件流,不支援錄製回放等。我們在平時的使用中,關注較多的是利用好Monkey的優勢,如不需原始碼,不需編譯就可以直接執行。

法寶2:Monkey之子——MonkeyRunner

Monkey雖然能實現部分的自動化測試任務,但本身有很多的侷限性,例如不支援截圖,點選事件是基於座標的,不支援錄製回放等。我們在實際應用中,儘量關注利用好Monkey測試的優勢。若平時的工作中遇到Monkey工具無法滿足的,這裡給大家推薦另一款工具MonkeyRunner。
同樣先簡單的介紹下MonkeyRunner的API,這裡重點介紹能夠實現上文Monkey指令碼的API,其餘的API感興趣的小夥伴可以自行查閱。
(1) 等待裝置連線:waitForConnection()
(2) 安裝apk應用:installPackage(String path)
(3) 啟動應用:startActivity(String packageName activityName)
(4) 點選事件:touch(int xPos, int yPos, dictionary type)
(5) 輸入事件:type(String text)
(6) 等待:sleep(int second)
(7) 截圖:takeSnapshot()
(8) 傳送鍵值:press(String name, dictionary type)

MokeyRunner編寫自動化測試指令碼

下面我們來看下,用MonkeyRunner實現的自動化指令碼。


# import monkeyrunner modules
from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice, MonkeyImage
# Parameters
txt1_x = 210
txt1_y = 200
txt2_x = 210
txt2_y = 280
txt3_x = 210
txt3_y = 420
txt4_x = 338
txt4_y = 476
submit_x = 100
submit_y = 540
type = 'DOWN_AND_UP'
seconds = 1
txt1_msg = 'Hello'
txt2_msg = 'MonkeyRunner' 
# package name and activity name
package = 'com.ringo.bugben'
activity = '.MainActivity'
component = package   '/' activity 
# Connect device
device = MonkeyRunner.waitForConnection() 
# Install bugben
device.installPackage('./bugben.apk')
print 'Install bugben.apk...' 
# Launch bugbendevice.startActivity(component)
print 'Launching bugben...' 
# Wait 1s
MonkeyRunner.sleep(seconds)
# Input txt1
device.touch(txt1_x, txt1_y, type)device.type(txt1_msg)
print 'Inputing txt1...' 
# Input txt2
device.touch(txt2_x, txt2_y, type)
device.type(txt2_msg)
print 'Inputing txt2...' 
#select bold and size
device.touch(txt3_x, txt3_y, type)
device.touch(txt4_x, txt4_y, type) 
# Wait 1s
MonkeyRunner.sleep(seconds) 
# Submitdevice.touch(submit_x, submit_y, type)
print 'Submiting...' 
# Wait 1s
MonkeyRunner.sleep(seconds) 
# Get the snapshot
picture = device.takeSnapshot()
picture.writeToFile('./HelloMonkeyRunner.png','png')
print 'Complete! See bugben_pic.png in currrent folder!' 
# Back to home
device.press('KEYCODE_HOME', type)
print 'back to home.'

將指令碼儲存為HelloMonkeyRunner.py,並和Bugben.apk一起拷貝到Android SDK的tools目錄下,執行monkeyrunner HelloMonkeyRunner.py
這裡寫圖片描述

執行完成後,效果如上,並且會在當前目錄生成HelloMonkeyRunner.png截圖。
MokeyRunner的錄製回放
首先是環境配置,在原始碼“~\sdk\monkeyrunner\scripts”目錄下有monkey_recorder.py和monkey_playback.py,將這兩個檔案(附件中有這兩檔案)拷貝到SDK的tools目錄下,就可以通過如下程式碼進行啟動:

monkeyrunner monkey_recorder.py

執行結果如下圖所示:
這裡寫圖片描述
下面用MonkeyRecorder提供的控制元件,來進行指令碼的錄製。
這裡寫圖片描述
錄製完成後,匯出指令碼儲存為HelloMonkeyRunnerRecorder.mr,用文字編輯器開啟程式碼如下:

TOUCH|{'x':317,'y':242,'type':'downAndUp',}
TYPE|{'message':'Hello',}TOUCH|{'x':283,'y':304,'type':'downAndUp',}
TYPE|{'message':'MonkeyRecorder',}
TOUCH|{'x':249,'y':488,'type':'downAndUp',}
TOUCH|{'x':375,'y':544,'type':'downAndUp',}
TOUCH|{'x':364,'y':626,'type':'downAndUp',}

指令碼錄製完畢,接來下看看回放指令碼是否正常。回放指令碼時執行以下命令:
monkeyrunner monkey_playback your_script.mr

由於指令碼中未加入拉起應用的程式碼,這裡執行前需手動拉起應用。
這裡寫圖片描述
]![這裡寫圖片描述
結果執行正常,符合我們的預期。

MonkeyRunner工具總結

MonkeyRunner有很多強大並好用的API,並且支援錄製回放和截圖操作。同樣它也不需原始碼,不需編譯就可以直接執行。但MonkeyRunner和Monkey類似,也是基於控制元件座標進行定位的,這樣的定位方式極易導致回放失敗。

法寶3:單元測試框架——Instrumentation

Monkey父子均可通過編寫相應的指令碼,在不依賴原始碼的前提下完成部分自動化測試的工作。但它們都是依靠控制元件座標進行定位的,在實際專案中,控制元件座標往往是最不穩定的,隨時都有可能因為程式設計師對控制元件位置的調整而導致指令碼執行失敗。怎樣可以不依賴座標來進行應用的自動化測試呢?下面就要亮出自動化測試的屠龍寶刀了——Instrumentation框架。
Instrumentation框架主要是依靠控制元件的ID來進行定位的,擁有成熟的用例管理系統,是Android主推的白盒測試框架。若想對專案進行深入的、系統的單元測試,基本上都離不開Instrumentation這把屠龍寶刀。
在瞭解Instrumentation框架之前,先對Android元件生命週期對應的回撥函式做個說明:
這裡寫圖片描述
從上圖可以看出,Activity處於不同狀態時,將呼叫不同的回撥函式。但Android API不提供直接呼叫這些回撥函式的方法,在Instrumentation中則可以這樣做。Instrumentation類通過“hooks”控制著Android元件的正常生命週期,同時控制Android系統載入應用程式。通過Instrumentation類我們可以在測試程式碼中呼叫這些回撥函式,就像在除錯該控制元件一樣一步一步地進入到該控制元件的整個生命週期中。
Instrumentation和Activity有點類似,只不過Activity是需要一個介面的,而Instrumentation並不是這樣的,我們可以將它理解為一種沒有圖形介面的,具有啟動能力的,用於監控其他類(用Target Package宣告)的工具類。
下面通過一個簡單的例子來講解Instrumentation的基本測試方法。
1. 首先建立專案名為HelloBugben的Project,類名為HelloBugbenActivity,程式碼如下:

package com.example.hellobugben;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextPaint;
import android.view.Menu;
import android.widget.TextView; 
public class HelloBugbenActivity extends Activity{         
private TextView textview1;         
private TextView textview2;        
@Override        
protectedvoidonCreate(Bundle savedInstanceState){         
super.onCreate(savedInstanceState);           
setContentView(R.layout.main);                    
String bugben_txt = "bugben";          
Boolean bugben_bold = true;         
Float bugben_size = (float)60.0;         
textview1 = (TextView)findViewById(R.id.textView1);                
textview2 = (TextView)findViewById(R.id.textView2);                
setTxt(bugben_txt);                
setTv1Bold(bugben_bold);                
setTv2Size(bugben_size);     
}              
publicvoidsetTv2Size(Float bugben_size){             
// TODO Auto-generated method stub            
TextPaint tp = textview2.getPaint();                
tp.setTextSize(bugben_size);      
}              
publicvoidsetTv1Bold(Boolean bugben_bold){               
// TODO Auto-generated method stub             
TextPaint tpPaint = textview1.getPaint();                          
tpPaint.setFakeBoldText(bugben_bold);       
}            
publicvoidsetTxt(String bugben_txt){              
// TODO Auto-generated method stub              
textview1.setText(bugben_txt);            
textview2.setText(bugben_txt);     
}
}

這個程式的功能很簡單,就是給2個TextView的內容設定不同的文字格式。
2. 對於測試工程師而言,HelloBugben是一個已完成的專案。接下來需建立一個測試專案,選擇“New->Other->Android Test Project”,命名為HelloBugbenTest,選擇要測試的目標專案為HelloBugben專案,然後點選Finish即可完成測試專案的建立。
這裡寫圖片描述
這裡寫圖片描述
可以注意到,該專案的包名自帶了com.example.hellobugben.test這個test標籤,這就說明該測試專案是針對HelloBugben所設定的。
開啟AndroidManifest可看到標籤,該標籤元素用來指定要測試的應用程式,自動將com.example.hellobugben設為targetPackage物件,程式碼清單如下:

<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"    
package="com.example.hellobugben.test"     
android:versionCode="1"      
android:versionName="1.0" >       
<uses-sdkandroid:minSdkVersion="8" />       
<instrumentation            
android:name="android.test.InstrumentationTestRunner"              
android:targetPackage="com.example.hellobugben" />      
<application             
android:icon="@drawable/ic_launcher"          
android:label="@string/app_name" >           
<uses-libraryandroid:name="android.test.runner" />    
</application></manifest>

在標籤中,android:name宣告瞭測試框架,android:targetPackage指定了待測專案包名。
下面來看一下,如何用Instrumentation框架編寫測試程式,程式碼如下:

package com.example.hellobugben.test;
import com.example.hellobugben.HelloBugbenActivity;
import com.example.hellobugben.R; 
import android.os.Handler;
import android.text.TextPaint;
import android.widget.TextView;
import android.test.ActivityInstrumentationTestCase2;
public classHelloBugbenTestBaseextendsActivityInstrumentationTestCase2<HelloBugbenActivity>{              
public HelloBugbenTestBase() {         
super(HelloBugbenActivity.class);  
}               
HelloBugbenActivity helloBugben;    
private Handler handler = null;       
private TextView textView1;     
private TextView textView2;          
String bugben_txt = "bugben";      
Boolean bugben_bold = true;      
Float bugben_sizeFloat = (float)20.0;      
Float value;          
@Override      
public void setUp() throws Exception{               
super.setUp();           
helloBugben = getActivity();            
textView1 = (TextView)helloBugben.findViewById(R.id.textView1);             
textView2 = (TextView)helloBugben.findViewById(R.id.textView2);             
handler = new Handler();    }            
@Override      
public voidtearDown()throws Exception{              
super.tearDown();      }              
public void testSetTxt(){         
new Thread(){           
public voidrun(){               
if (handler != null) {                                                
handler.post(runnableTxt);                           
}     
}              
}.start();          
String cmpTxtString = textView1.getText().toString();              
assertTrue(cmpTxtString.compareToIgnoreCase(bugben_txt) == 0);     
}              
public void testSetBold(){           
helloBugben.setTv1Bold(bugben_bold);         
TextPaint tp = textView1.getPaint();          
Boolean cmpBold = tp.isFakeBoldText();                             
assertTrue(cmpBold);      
}                  
publicvoidtestSetSize(){             
helloBugben.setTv2Size(bugben_sizeFloat);             
Float cmpSizeFloat = textView2.getTextSize();                      
assertTrue(cmpSizeFloat.compareTo(bugben_sizeFloat) == 0);    
}                 
Runnable runnableTxt = new Runnable() {                               
@Override           
publicvoidrun(){                
// TODO Auto-generated method stub            
helloBugben.setTxt(bugben_txt);        
}    
};
}

上述程式碼中,我們首先引入import android.test.ActivityInstrumentationTestCase2。其次讓HelloBugbenTestBase繼承自ActivityInstrumentationTestCase2這個類。接著在setUp()方法中通過getActivity()方法獲取待測專案的例項,並通過textview1和textview2獲取兩個TextView控制元件。最後編寫3個測試用例:控制文字設定測試testSetText()、字型加粗屬性測試testSetBold、字型大小屬性測試testSetSize()。這裡用到的關鍵方法是Instrumentation API裡面的getActivity()方法,待測的Activity在沒有呼叫此方法的時候是不會啟動的。
眼尖的小夥伴可能已經發現控制文字設定測試這裡啟用了一個新執行緒,這是因為在Android中相關的view和控制元件不是執行緒安全的,必須單獨在新的執行緒中做處理,不然會報

android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views

這個錯誤。所以需要啟動新執行緒進行處理,具體步驟如下:
1) 在setUp()方法中建立Handler物件,程式碼如下:

public void setUp() throws Exception{           
super.setUp();            
handler = new Handler(); 
}

2) 建立Runnable物件,在Runnable中進行控制元件文字設定,程式碼如下:

 Runnable runnableTxt = new Runnable() {                        
@Override         
public void run(){                  
// TODO Auto-generated method stub                                 
helloBugben.setTxt(bugben_txt);     
}  
};

3) 在具體測試方法中通過呼叫runnable物件,實現文字設定,程式碼如下:

 new Thread(){    
public void run() {                                  
if (handler != null) {                                                    
handler.post(runnableTxt);                   
}                                  
}            
}.start(); 

我們執行一下結果,結果截圖如下:
這裡寫圖片描述

可以看到3個測試用例結果執行正常。
可能有小夥伴要問,程式中為啥要繼承ActivityInstrumentationTestCase2呢?我們先看一下ActivityInstrumentationTestCase2的繼承結構:
java.lang.Object
junit.framework.Assert
junit.framework.TestCase
android.test.InstrumentationTestCase
android.test.ActivityTestCase
android.test.ActivityInstrumentationTestCase2

ActivityInstrumentationTestCase2允許InstrumentationTestCase. launchActivity來啟動被測試的Activity。而且ActivityInstrumentationTestCase2還支援在新的UI執行緒中執行測試方法,能注入Intent物件到被測試的Activity中,這樣一來,我們就能直接操作被測試的Activity了。正因為ActivityInstrumentationTestCase2有如此出眾的有點,它才成功取代了比它早出世的哥哥:ActivityInstrumentationTestCase,成為了Instrumentation測試的基礎。

Instrumentation測試框架實戰

瞭解完Instrumentation的基本測試方法後,我們來看一下如何運用Instrumentation框架完成前文Monkey父子完成的自動化測試任務。
這裡寫圖片描述
這裡寫圖片描述

  1. 首先建立專案名為Bugben的Project,類名為MainActivity,程式碼如下:
package com.ringo.bugben;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RadioButton; 
public classMainActivityextendsActivity{       
private EditText editText1 = null;         
private EditText editText2 = null;       
private RadioButton bold = null;       
private RadioButton  small = null;      
private Button button = null;        
@Override    
protected void onCreate(Bundle savedInstanceState){           
super.onCreate(savedInstanceState);            
setContentView(R.layout.main);            
editText1 = (EditText)findViewById(R.id.editText1);         
editText2 = (EditText)findViewById(R.id.editText2);        
button = (Button)findViewById(R.id.mybutton1);         
bold = (RadioButton)findViewById(R.id.radioButton1);               
small = (RadioButton)findViewById(R.id.radioButton3);                     
button.setOnClickListener(new OnClickListener(){                       
@Override                      
publicvoidonClick(View v){                
Log.v("Ringo", "Press Button");                                    
String isBold = bold.isChecked() ? "bold" : "notbold";            
String wordSize = small.isChecked() ? "small" : "big";             
// TODO Auto-generated method stub                                 
Intent intent = new Intent(MainActivity.this, OtherActivity.class);                                 
intent.putExtra("text1", editText1.getText().toString());          
intent.putExtra("text2", editText2.getText().toString());          
intent.putExtra("isBold", isBold);                                 
intent.putExtra("wordSize", wordSize);                             
startActivity(intent);        
}           
});   
}
}
  1. 在建立一個名為OtherActivity的類,點選提交按鈕後,跳轉到這個介面,程式碼如下:
package com.ringo.bugben;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextPaint;
import android.widget.TextView; 
public classOtherActivityextendsActivity{       
private TextView textView2 = null;        
private TextView textView3 = null;         
Boolean bugben_bold = true;      
Boolean bugben_notbold = false;         
Float bugben_small_size = (float)20.0;      
Float bugben_big_size = (float)60.0;          
@Override   protectedvoidonCreate(Bundle savedInstanceState){     
super.onCreate(savedInstanceState);        
setContentView(R.layout.other);         
textView2 = (TextView)findViewById(R.id.textView2);    
textView3 = (TextView)findViewById(R.id.textView3);                
Intent data = getIntent();       
textView2.setText(data.getStringExtra("text1"));     
textView3.setText(data.getStringExtra("text2"));      
if (data.getStringExtra("isBold").equalsIgnoreCase("bold")) {                
TextPaint tPaint = textView2.getPaint();                           
tPaint.setFakeBoldText(bugben_bold);            
}else{             
TextPaint tPaint = textView2.getPaint();                           tPaint.setFakeBoldText(bugben_notbold);          
}           
if (data.getStringExtra("wordSize").equalsIgnoreCase("small")) {       
TextPaint tPaint = textView3.getPaint();                     
tPaint.setTextSize(bugben_small_size);         
}else{                   
TextPaint tPaint = textView3.getPaint();                          
tPaint.setTextSize(bugben_big_size);        
}    
}
}

3.接下來需建立一個測試專案,命名為BugbenTestBase,選擇要測試的目標專案為Bugben專案,然後點選Finish即可完成測試專案的建立。
這裡寫圖片描述
這裡寫圖片描述
在com.ringo.bugben.test包中新增BugbenTestBase這個類,類的程式碼如下:

package com.ringo.bugben.test;
import com.ringo.bugben.MainActivity;
import com.ringo.bugben.OtherActivity;
import com.ringo.bugben.R;
import android.app.Instrumentation.ActivityMonitor;
import android.content.Intent;
import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.text.TextPaint;
import android.util.Log;
import android.widget.Button;import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.TextView;
public class BugbenTestBase extends ActivityInstrumentationTestCase2<MainActivity>{        
publicBugbenTestBase(){            
super(MainActivity.class);     
}             
MainActivity mainActivity;   
OtherActivity otherActivity;        
private EditText txt1;     
private EditText txt2;      
private RadioButton bold;      
private RadioButton notbold;     
private RadioButton small;      
private RadioButton big;     
private Button subButton;     
private TextView textView1;     
private TextView textView2;      
// 輸入值      
String bugben_txt1 = "RingoYan";    
String bugben_txt2 = "自動化測試";    
Boolean bugben_bold = true;       
Boolean bugben_notbold = false;      
Float bugben_small_size = (float)20.0;     
Float bugben_big_size = (float)60.0;        
@Override      
public void setUp() throws Exception{                
super.setUp();                           
// 啟動MainActivity              
Intent intent = new Intent();           
intent.setClassName("com.ringo.bugben", MainActivity.class.getName());                 
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);             
mainActivity = (MainActivity)getInstrumentation().startActivitySync(intent);             
// 通過mainActivity的findViewById獲取MainActivity介面的控制元件                      
txt1 = (EditText)mainActivity.findViewById(R.id.editText1);                      
txt2 = (EditText)mainActivity.findViewById(R.id.editText2);                    
bold = (RadioButton)mainActivity.findViewById(R.id.radioButton1);                    
notbold = (RadioButton)mainActivity.findViewById(R.id.radioButton2);               
small = (RadioButton)mainActivity.findViewById(R.id.radioButton3);                     
big = (RadioButton)mainActivity.findViewById(R.id.radioButton4);                     
subButton = (Button)mainActivity.findViewById(R.id.mybutton1);    
}                      
@Override         
publicvoidtearDown()throws Exception{              
super.tearDown();       
}                  
// 提交測試     
public void testSubmit()throws Throwable{            
Log.v("Ringo", "test normal submit");              
// 新增一個監聽器,監視OtherActivity的啟動                    
ActivityMonitor bugbenMonitor = getInstrumentation().addMonitor(                    
OtherActivity.class.getName(), null, false);             
// 要操作待測程式的UI必須在runTestOnUiThread中執行             
runTestOnUiThread(new Runnable() {                              
@Override                         
publicvoidrun(){                 
// TODO Auto-generated method stub                              
txt1.setText(bugben_txt1);                                  
txt2.setText(bugben_txt2);                                  
bold.setChecked(true);                                      
big.setChecked(true);                                                                
// 等待500毫秒,避免程式響應慢出錯                             
SystemClock.sleep(500);                                                                  
// 點選提交按鈕                  
subButton.performClick();          
}              
});                           
// 從ActivityMonitor監視器中獲取OtherActivity的例項           
otherActivity = (OtherActivity)getInstrumentation().waitForMonitor(bugbenMonitor);                
// 獲取的OtherActivity例項應不為空         
assertTrue(otherActivity != null);                          
textView1 = (TextView)otherActivity.findViewById(R.id.textView2);                  
textView2 = (TextView)otherActivity.findViewById(R.id.textView3);         
assertEquals(bugben_txt1, textView1.getText().toString());                 
assertEquals(bugben_txt2, textView2.getText().toString());                            
TextPaint tp = textView1.getPaint();        
Boolean cmpBold = tp.isFakeBoldText();          
assertTrue(cmpBold);            
Float cmpSize = textView2.getTextSize();         
assertTrue(cmpSize.compareTo(bugben_big_size) == 0);             
// 等待500毫秒,避免程式響應慢出錯                
SystemClock.sleep(5000);   
}
}

上述程式碼中,共包括自動化測試需要進行的5個步驟,具體如下:
(1) 啟動應用:通過Intent物件setClassName()方法設定包名和類名,通過setFlags()方法設定標示,然後通過getInstrumentation()的startActivitySync(intent)來啟動應用,進入到主介面。
(2) 編輯控制元件:在Android中相關的view和控制元件不是執行緒安全的,所以必須單獨在新的執行緒中做處理。程式碼中我們在runTestOnUiThread(new Runnable())中的run()方法中執行的。
(3) 提交結果:點選提交按鈕進行結果的提交,由於點選按鈕也屬於介面操作,所以也需要在runTestOnUiThread這個執行緒中完成。
(4) 介面跳轉:這是Instrumentation自動化測試中最需要注意的一個點,特別是如何確認介面已經發生了跳轉。在Instrumentation中可以通過設定Monitor監視器來確認。程式碼如下:

ActivityMonitor bugbenMonitor = getInstrumentation().addMonitor(        
OtherActivity.class.getName(), null, false);

然後通過waitForMonitor方法等待介面跳轉。

otherActivity = (OtherActivity)getInstrumentation().waitForMonitor(bugbenMonitor);

若返回結果otherActivity物件不為空,說明跳轉正常。
(5) 驗證顯示:跳轉後,通過assertEquals()或assertTrue()方法來判斷顯示的正確性。
我們執行一下結果,結果截圖如下:
這裡寫圖片描述
]![這裡寫圖片描述
這裡寫圖片描述

Instrumentation工具總結

Instrumentation框架的整體執行流程圖如下:
這裡寫圖片描述
Instrumentation是基於原始碼進行指令碼開發的,測試的穩定性好,可移植性高。正因為它是基於原始碼的,所以需要指令碼開發人員對Java語言、Android框架執行機制、Eclipse開發工具都非常熟悉。Instrumentation框架本身不支援多應用的互動,例如測試“通過簡訊中的號碼去撥打電話”這個用例,被測應用將從簡訊應用介面跳轉到撥號應用介面,但Instrumentation沒有辦法同事控制簡訊和撥號兩個應用,這是因為Android系統自身的安全性限制,禁止多應用的程序間相互訪問。

法寶4:終極自動化測試框架——UIAutomator

鑑於Instrumentation框架需要讀懂專案原始碼、指令碼開發難度較高並且不支援多應用互動,Android官網亮出了自動化測試的王牌——UIAutomator,並主推這個自動化測試框架。該框架無需專案原始碼,指令碼開發效率高且難度低,並且支援多應用的互動。當UIAutomator面世後,Instrumentation框架回歸到了其單元測試框架的本來位置。
下面我們來看一下這個框架是如何執行起來的。首先執行位於Android SDK的tools目錄下的uiautomatorviewer.bat,可以看到啟動介面。
這裡寫圖片描述

啟動bugben應用後,點選這裡寫圖片描述
這個圖示來採集手機的介面資訊,如下所示:
這裡寫圖片描述
我們可以看到,用uiautomatorviewer捕捉到的控制元件非常清晰,很方便元素位置的定位。在UIAutomator框架中,測試程式與待測程式之間是鬆耦合關係,即完全不需要獲取待測程式的控制元件ID,只需對控制元件的文字(text)、描述(content-desc)等資訊進行識別即可。
在進行實戰之前,我們先看一下UIAutomator的API部分,由以下架構圖組成。
這裡寫圖片描述
下面來看下如何利用該框架建立測試工程。
1. 建立BugBenTestUIAuto專案,右鍵點選專案並選擇Properties > Java Build Path
點選Add Library > Junit > Junit3,新增Junit框架。

點選Add External Jar,並導航到Android SDK目錄下,選擇platforms目錄下面的android.jar和UIAutomator.jar兩個檔案。

這裡寫圖片描述
2. 設定完成後,可以開始編寫專案測試的程式碼,具體如下:

package com.ringo.bugben.test;
import java.io.File;import android.util.Log;
import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiSelector;
import com.android.uiautomator.testrunner.UiAutomatorTestCase;;
public class BugBenTest extends UiAutomatorTestCase{   
public BugBenTest (){      super();  }   
String bugben_txt1 = "xiaopangzhu";  
String bugben_txt2 = "bugben";  
String storePath = "/data/local/tmp/displayCheck.png";  
String testCmp = "com.ringo.bugben/.MainActivity";   
@Override  
public void setUp ()throws Exception{      
super.setUp();      
// 啟動MainActivity       
startApp(testCmp);  }   
private int startApp(String componentName){      
StringBuffer sBuffer = new StringBuffer();      
sBuffer.append("am start -n ");      
sBuffer.append(componentName);      
int ret = -1;     
try {        
Process process = Runtime.getRuntime().exec(sBuffer.toString());        
ret = process.waitFor();      
} 
catch (Exception e) {        
// TODO: handle exception         
e.printStackTrace();    
}      
return ret; 
}   
@Override   
public void tearDown()throws Exception{      
super.tearDown();  
}   
// 提交文字測試   
public void testSubmitTest() throws UiObjectNotFoundException{Log.v  ("Ringo", "test change the textview's txt and size by UIAutomator");   
// 獲取文字框1並賦值   
UiObject bugben_et1 = new UiObject(new UiSelector().text("Ringoyan"));  
if(bugben_et1.exists() && bugben_et1.isEnabled()){     
bugben_et1.click();     
bugben_et1.setText(bugben_txt1);  
}else{     
Log.e("Ringo", "can not find bugben_et1");  }  
// 獲取文字框2並賦值   
UiObject bugben_et2 = new UiObject(new UiSelector().text("18888"));  
if(bugben_et2.exists() && bugben_et2.isEnabled()){     
bugben_et2.click();     
bugben_et2.setText(bugben_txt2);  
}else{     
Log.e("Ringo", "can not find bugben_et2");  
}   
// 獲取加粗選項並賦值   UiObject bugben_bold = new UiObject(new UiSelector().text("加粗"));  
if(bugben_bold.exists() && bugben_bold.isEnabled()){     
bugben_bold.click();  
}else{     
Log.e("Ringo", "can not find 加粗");  
}   
// 獲取大號字型選項並賦值   
UiObject bugben_big = new UiObject(new UiSelector().text("大號"));  
if(bugben_big.exists() && bugben_big.isEnabled()){     
bugben_big.click();  
}else{     
Log.e("Ringo", "can not find 大號");  }       
// 獲取提交按鈕並跳轉   
UiObject subButton = new UiObject(new UiSelector().text("提交"));  
if(subButton.exists() && subButton.isEnabled()){    
subButton.clickAndWaitForNewWindow();  
}else{    
Log.e("Ringo", "can not find 提交");}     
// 獲取文字框1文字   
UiObject bugben_tv1 = new UiObject(new UiSelector()   
.className("android.widget.LinearLayout")   
.index(0)
.childSelector(new UiSelector()   
.className("android.widget.FrameLayout")   
.index(1))   
.childSelector(new UiSelector()  
.className("android.widget.TextView")   
.instance(0)));      
// 獲取文字框2文字    
UiObject bugben_tv2 = new UiObject(new UiSelector()   
.className("android.widget.LinearLayout")   
.index(0).childSelector(new UiSelector()   
.className("android.widget.FrameLayout")   
.index(1))   
.childSelector(new UiSelector()   
.className("android.widget.TextView")   
.instance(1)));      
// 驗證    
if (bugben_tv1.exists() && bugben_tv1.isEnabled()) {       
assertEquals(bugben_txt1, bugben_tv1.getText().toString());   
}else{       
Log.e("Ringo", "can not find bugben_tv1"); 
}   
if (bugben_tv2.exists() && bugben_tv2.isEnabled()) {       
assertEquals(bugben_txt2, bugben_tv2.getText().toString());   
}else{       
Log.e("Ringo", "can not find bugben_tv2"); 
}   
// 截圖    
File displayPicFile = new File(storePath);   
Boolean displayCap = UiDevice.getInstance().takeScreenshot(displayPicFile);   
assertTrue(displayCap);  
}
}

上述程式碼中,我們首先引入import com.android.uiautomator.testrunner.UiAutomatorTestCase類,並讓BugbenTest繼承自UiAutomatorTestCase這個類。同樣,我們來看下UiAutomator框架下自動化測試進行的5個步驟,具體如下:
(1) 啟動應用:於Instrumentation框架不同,UiAutomator是通過命令列進行應用啟動的。
am start –n 包名/.應用名

(2) 編輯控制元件:UiAutomator框架中,控制元件的編輯相對簡單,直接通過UiSelector的text()方法找到對應的控制元件,然後呼叫控制元件的setText()即可對其賦值。

UiObject bugben_et1 = new UiObject(new UiSelector().text("Ringoyan"));           
if(bugben_et1.exists() && bugben_et1.isEnabled()){              
bugben_et1.click();               
bugben_et1.setText(bugben_txt1);    
}

(3) 提交結果:點選提交按鈕進行結果的提交,也是通過UiSelector的text()方法找到對應的控制元件,然後呼叫clickAndWaitForNewWindow()方法來等待跳轉完成。

UiObject subButton = new UiObject(new UiSelector().text("提交"));                 
if(subButton.exists() && subButton.isEnabled()){                
subButton.clickAndWaitForNewWindow();            
}

(4) 介面跳轉元素獲取:用uiautomatorviewer捕捉跳轉後的控制元件,例如捕捉跳轉後的文字1:
這裡寫圖片描述

UiObject bugben_tv1 = new 
UiObject(new UiSelector()  
.className("android.widget.LinearLayout")  
.index(0)  
.childSelector(new UiSelector()  
.className("android.widget.FrameLayout")  
.index(1))  
.childSelector(new UiSelector()  
.className("android.widget.TextView")  
.instance(0)));

(5) 驗證顯示:跳轉後,通過assertEquals()或assertTrue()方法來判斷顯示的正確性。

if (bugben_tv1.exists() && bugben_tv1.isEnabled())    
{assertEquals(bugben_txt1, bugben_tv1.getText().toString());}

至此核心程式碼部分已編寫完畢。UIAutomator有一個麻煩之處:沒法通過Eclipse直接編譯。可以藉助於一系列命令列進行編譯,詳細步驟如下:
1) 通過如下命令建立編譯的build.xml檔案

android create uitest-project –n BugBenTestUIAuto –t 1 –p "E:\workspace\BugBenTestUIAuto"

建立完成後,重新整理BugBenTestUIAuto專案,得到如下圖:
這裡寫圖片描述
這裡寫圖片描述
開啟build.xml會看到,編譯專案名為BugBenTestUIAuto。
2) 設定SDK的路徑:

set ANDROID_HOME="E:\sdk\android-sdk-windows"

3) 進入測試目錄,然後進行編譯:

cd /d E:\workspace\android\BugBenTestUIAutoant build

編譯完成後,再次重新整理專案,你將看到BugBenTestUIAuto.jar包生成在bin目錄下了,如圖:
這裡寫圖片描述
4) 將生成的jar包推送到手機端

adb push E:\workspace\android\BugBenTestUIAuto\bin\BugBenTestUIAuto.jar /data/local/tmp/

5) 在手機端執行自動化指令碼,即jar包中的測試用例,命令列如下:

adb shell uiautomator runtest BugBenTestUIAuto.jar -c com.ringo.bugben.test.BugBenTest

執行結果如下,返回OK表示執行成功。
這裡寫圖片描述
6) 最後,將執行後的截圖從手機端拷貝到PC上

adb pull /data/local/tmp/displayCheck.png E:\workspace\android\BugBenTestUIAuto

這裡寫圖片描述
至此整個程式碼就編譯和執行完畢,如果覺得除錯時反覆修改和編譯比較麻煩,可以將以上指令碼寫成一個批處理檔案。
UIAutomator工具總結
相比於Instrumentation工具,UIAutomator工具更靈活一些,它不需要專案原始碼,擁有視覺化的介面和視覺化的樹狀層級列表,極大降低了自動化測試指令碼開發的門檻。並且UIAutomator支援多應用的互動,彌補了Instrumentation工具的不足。但UIAutomator難以捕捉到控制元件的顏色、字型粗細、字號等資訊,要驗證該類資訊的話,需要通過截圖的方式進行半自動驗證。同時,UIAutomator的除錯相比Instrumentation要困難。所以在平時的測試過程中,建議將兩者結合起來使用,可達到更佳的效果!

關於騰訊WeTest (wetest.qq.com)

騰訊WeTest是騰訊遊戲官方推出的一站式遊戲測試平臺,用十年騰訊遊戲測試經驗幫助廣大開發者對遊戲開發全生命週期進行質量保障。騰訊WeTest提供:適配相容測試;雲端真機除錯;安全測試;耗電量測試;伺服器效能測試;輿情監控等服務。