作者:Ringoyan,騰訊測(cè)試開(kāi)發(fā)工程師。先后為植物大戰(zhàn)僵尸Online,糖果傳奇等游戲擔(dān)任測(cè)試經(jīng)理,其負(fù)責(zé)的“我叫MT2”測(cè)試項(xiàng)目曾獲騰訊互動(dòng)娛樂(lè)精品文化獎(jiǎng)銀獎(jiǎng)。目前擔(dān)任騰訊WeTest測(cè)試經(jīng)理。擅長(zhǎng)領(lǐng)域:App的自動(dòng)化測(cè)試和Web的安全測(cè)試工作。
注:核心內(nèi)容轉(zhuǎn)自許奔的《深入理解Android自動(dòng)化測(cè)試》,本書(shū)將在許奔公眾號(hào)“巴哥奔”中全文連載。
商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系騰訊WeTest獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
WeTest導(dǎo)讀
說(shuō)起Android的自動(dòng)化測(cè)試,相信有很多小伙伴都接觸過(guò)或者有所耳聞,本文從框架最基本的功能介紹及API的使用入手,結(jié)合簡(jiǎn)單的項(xiàng)目實(shí)戰(zhàn)來(lái)幫忙大家對(duì)該框架進(jìn)一步理解和加深印象。下面讓我們來(lái)一睹標(biāo)準(zhǔn)App的四大自動(dòng)化測(cè)試法寶的風(fēng)采!
法寶1:穩(wěn)定性測(cè)試?yán)鳌狹onkey
要想發(fā)布一個(gè)新版本,得先通過(guò)穩(wěn)定性測(cè)試。理想情況是找個(gè)上幼兒園的弟弟妹妹,打開(kāi)應(yīng)用把手機(jī)交給他,讓他胡亂的玩,看你的程序能不能接受這樣的折騰。但是我們身邊不可能都有正太和蘿莉,也不能保證他們拿到手機(jī)后不是測(cè)試軟件的健壯性,反而測(cè)試你的手機(jī)經(jīng)不經(jīng)摔,這與我們的期望差太遠(yuǎn)了…
Google公司考慮到我們的需要,開(kāi)發(fā)出了Monkey這個(gè)工具。但在很多人的印象中,Monkey測(cè)試就是讓設(shè)備隨機(jī)的亂點(diǎn),事件都是隨機(jī)產(chǎn)生的,不帶任何人的主觀性。很少有人知道,其實(shí)Monkey也可以用來(lái)做簡(jiǎn)單的自動(dòng)化測(cè)試工作。
Mokey基本功能介紹
首先,介紹下Monkey的基本使用,如果要發(fā)送500個(gè)隨機(jī)事件,只需運(yùn)行如下命令:
adb shell monkey 500
插上手機(jī)運(yùn)行后,大家是不是發(fā)現(xiàn)手機(jī)開(kāi)始瘋狂的運(yùn)行起來(lái)了。So Easy!
在感受完Monkey的效果后,發(fā)現(xiàn)這“悟空”太調(diào)皮了,根本招架不住啊!是否有類(lèi)似“緊箍咒”這種約束類(lèi)命令,讓這只猴子在某個(gè)包或類(lèi)中運(yùn)行呢?要想Monkey牢牢的限制在某個(gè)包中,命令也很簡(jiǎn)單:
adb shell monkey –p your-package-name 500
-p后面接你程序的包名。多想限制在多個(gè)包中,可以在命令行中添加多個(gè)包:
adb shell monkey –p your-package1-name –p your-package2-name 500
這樣“悟空”就飛不出你的五指山了。
Mokey編寫(xiě)自動(dòng)化測(cè)試腳本
若控制不住“悟空”,只讓它隨機(jī)亂點(diǎn)的話,Monkey是替代不了黑盒測(cè)試用例的。我們能不能想些辦法,控制住“悟空”讓他做些簡(jiǎn)單的自動(dòng)化測(cè)試的工作呢?下面來(lái)看一下,如何用Monkey來(lái)編寫(xiě)腳本。
先簡(jiǎn)單介紹下Monkey的API,若有需要詳細(xì)了解的小伙伴,可自行百度或谷歌一下查閱哈。
(1) 軌跡球事件:DispatchTrackball(參數(shù)1~參數(shù)12)
(2) 輸入字符串事件:DispatchString(String text)
(3) 點(diǎn)擊事件:DispatchPointer(參數(shù)1~參數(shù)12)
(4) 啟動(dòng)應(yīng)用:LaunchActivity(String pkg_name, String class_name)
(5) 等待事件:UserWait(long sleeptime)
(6) 按下鍵值:DispatchPress(int keyCode)
(7) 長(zhǎng)按鍵值:LongPress(int keyCode)
(8) 發(fā)送鍵值:DispatchKey(參數(shù)1~參數(shù)8)
(9) 打開(kāi)軟鍵盤(pán):DispatchFlip(Boolean keyboardOpen)
了解完常用API后,我們來(lái)看一下Monkey腳本的編寫(xiě)規(guī)范。Monkey Script是按照一定的語(yǔ)法規(guī)則編寫(xiě)的有序的用戶事件流,使用于Monkey命令工具的腳本。Monkey腳本一般以如下4條語(yǔ)句開(kāi)頭:
# Start Script
type = user #指明腳本類(lèi)型
count = 10 #腳本執(zhí)行次數(shù)
speed = 1.0 #命令執(zhí)行速率
start data >> #用戶腳本入口,下面是用戶自己編寫(xiě)的腳本```
下面來(lái)看一個(gè)簡(jiǎn)單應(yīng)用的實(shí)戰(zhàn),實(shí)現(xiàn)的效果很簡(jiǎn)單,就是隨便輸入文本,選擇選項(xiàng)再進(jìn)行提交,提交后要驗(yàn)證提交后的效果。


Start
Script
type = user
count = 10
speed = 1.0
start data >>LaunchActivity(com.ringo.bugben,com.ringo.bugben.MainActivity)
點(diǎn)擊文本框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內(nèi)容
captureDispatchString(Hello)
點(diǎn)擊文本框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內(nèi)容
captureDispatchString(Ringo)
點(diǎn)擊加粗
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)
點(diǎn)擊大號(hào)
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)
點(diǎn)擊提交
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文件,然后將該腳本推送到手機(jī)的sd卡里。
adb push HelloMonkey /mnt/sdcard/
然后運(yùn)行:
adb shell monkey -v -f /mnt/sdcard/HelloMonkey 1
腳本后面的數(shù)字1表示運(yùn)行該腳本的次數(shù)。小伙伴們可以安裝附件里的Bugben.apk再執(zhí)行下腳本感受下哦!
Monkey工具總結(jié)
Monkey可以編寫(xiě)腳本做簡(jiǎn)單的自動(dòng)化測(cè)試,但局限性非常大,例如無(wú)法進(jìn)行截屏操作,不能簡(jiǎn)單的支持插件的編寫(xiě),沒(méi)有好的辦法控制事件流,不支持錄制回放等。我們?cè)谄綍r(shí)的使用中,關(guān)注較多的是利用好Monkey的優(yōu)勢(shì),如不需源碼,不需編譯就可以直接運(yùn)行。
法寶2:Monkey之子——MonkeyRunner
Monkey雖然能實(shí)現(xiàn)部分的自動(dòng)化測(cè)試任務(wù),但本身有很多的局限性,例如不支持截屏,點(diǎn)擊事件是基于坐標(biāo)的,不支持錄制回放等。我們?cè)趯?shí)際應(yīng)用中,盡量關(guān)注利用好Monkey測(cè)試的優(yōu)勢(shì)。若平時(shí)的工作中遇到Monkey工具無(wú)法滿足的,這里給大家推薦另一款工具M(jìn)onkeyRunner。
同樣先簡(jiǎn)單的介紹下MonkeyRunner的API,這里重點(diǎn)介紹能夠?qū)崿F(xiàn)上文Monkey腳本的API,其余的API感興趣的小伙伴可以自行查閱。
(1) 等待設(shè)備連接:waitForConnection()
(2) 安裝apk應(yīng)用:installPackage(String path)
(3) 啟動(dòng)應(yīng)用:startActivity(String packageName+activityName)
(4) 點(diǎn)擊事件:touch(int xPos, int yPos, dictionary type)
(5) 輸入事件:type(String text)
(6) 等待:sleep(int second)
(7) 截圖:takeSnapshot()
(8) 發(fā)送鍵值:press(String name, dictionary type)
MokeyRunner編寫(xiě)自動(dòng)化測(cè)試腳本
下面我們來(lái)看下,用MonkeyRunner實(shí)現(xiàn)的自動(dòng)化腳本。
# 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目錄下,執(zhí)行monkeyrunner HelloMonkeyRunner.py
執(zhí)行完成后,效果如上,并且會(huì)在當(dāng)前目錄生成HelloMonkeyRunner.png截圖。
MokeyRunner的錄制回放
首先是環(huán)境配置,在源碼“~\sdk\monkeyrunner\scripts”目錄下有monkey_recorder.py和monkey_playback.py,將這兩個(gè)文件(附件中有這兩文件)拷貝到SDK的tools目錄下,就可以通過(guò)如下代碼進(jìn)行啟動(dòng):
monkeyrunner monkey_recorder.py
運(yùn)行結(jié)果如下圖所示:
下面用MonkeyRecorder提供的控件,來(lái)進(jìn)行腳本的錄制。
錄制完成后,導(dǎo)出腳本保存為HelloMonkeyRunnerRecorder.mr,用文本編輯器打開(kāi)代碼如下:
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',}
腳本錄制完畢,接來(lái)下看看回放腳本是否正常。回放腳本時(shí)執(zhí)行以下命令:
monkeyrunner monkey_playback your_script.mr
由于腳本中未加入拉起應(yīng)用的代碼,這里運(yùn)行前需手動(dòng)拉起應(yīng)用。
結(jié)果運(yùn)行正常,符合我們的預(yù)期。
MonkeyRunner工具總結(jié)
MonkeyRunner有很多強(qiáng)大并好用的API,并且支持錄制回放和截圖操作。同樣它也不需源碼,不需編譯就可以直接運(yùn)行。但MonkeyRunner和Monkey類(lèi)似,也是基于控件坐標(biāo)進(jìn)行定位的,這樣的定位方式極易導(dǎo)致回放失敗。
法寶3:?jiǎn)卧獪y(cè)試框架——Instrumentation
Monkey父子均可通過(guò)編寫(xiě)相應(yīng)的腳本,在不依賴(lài)源碼的前提下完成部分自動(dòng)化測(cè)試的工作。但它們都是依靠控件坐標(biāo)進(jìn)行定位的,在實(shí)際項(xiàng)目中,控件坐標(biāo)往往是最不穩(wěn)定的,隨時(shí)都有可能因?yàn)槌绦騿T對(duì)控件位置的調(diào)整而導(dǎo)致腳本運(yùn)行失敗。怎樣可以不依賴(lài)坐標(biāo)來(lái)進(jìn)行應(yīng)用的自動(dòng)化測(cè)試呢?下面就要亮出自動(dòng)化測(cè)試的屠龍寶刀了——Instrumentation框架。
Instrumentation框架主要是依靠控件的ID來(lái)進(jìn)行定位的,擁有成熟的用例管理系統(tǒng),是Android主推的白盒測(cè)試框架。若想對(duì)項(xiàng)目進(jìn)行深入的、系統(tǒng)的單元測(cè)試,基本上都離不開(kāi)Instrumentation這把屠龍寶刀。
在了解Instrumentation框架之前,先對(duì)Android組件生命周期對(duì)應(yīng)的回調(diào)函數(shù)做個(gè)說(shuō)明:
從上圖可以看出,Activity處于不同狀態(tài)時(shí),將調(diào)用不同的回調(diào)函數(shù)。但Android API不提供直接調(diào)用這些回調(diào)函數(shù)的方法,在Instrumentation中則可以這樣做。Instrumentation類(lèi)通過(guò)“hooks”控制著Android組件的正常生命周期,同時(shí)控制Android系統(tǒng)加載應(yīng)用程序。通過(guò)Instrumentation類(lèi)我們可以在測(cè)試代碼中調(diào)用這些回調(diào)函數(shù),就像在調(diào)試該控件一樣一步一步地進(jìn)入到該控件的整個(gè)生命周期中。
Instrumentation和Activity有點(diǎn)類(lèi)似,只不過(guò)Activity是需要一個(gè)界面的,而Instrumentation并不是這樣的,我們可以將它理解為一種沒(méi)有圖形界面的,具有啟動(dòng)能力的,用于監(jiān)控其他類(lèi)(用Target Package聲明)的工具類(lèi)。
下面通過(guò)一個(gè)簡(jiǎn)單的例子來(lái)講解Instrumentation的基本測(cè)試方法。
- 首先建立項(xiàng)目名為HelloBugben的Project,類(lèi)名為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);
}
}
這個(gè)程序的功能很簡(jiǎn)單,就是給2個(gè)TextView的內(nèi)容設(shè)置不同的文本格式。
- 對(duì)于測(cè)試工程師而言,HelloBugben是一個(gè)已完成的項(xiàng)目。接下來(lái)需創(chuàng)建一個(gè)測(cè)試項(xiàng)目,選擇“New->Other->Android Test Project”,命名為HelloBugbenTest,選擇要測(cè)試的目標(biāo)項(xiàng)目為HelloBugben項(xiàng)目,然后點(diǎn)擊Finish即可完成測(cè)試項(xiàng)目的創(chuàng)建。
可以注意到,該項(xiàng)目的包名自帶了com.example.hellobugben.test這個(gè)test標(biāo)簽,這就說(shuō)明該測(cè)試項(xiàng)目是針對(duì)HelloBugben所設(shè)置的。
打開(kāi)AndroidManifest可看到<Instrumentation>標(biāo)簽,該標(biāo)簽元素用來(lái)指定要測(cè)試的應(yīng)用程序,自動(dòng)將com.example.hellobugben設(shè)為targetPackage對(duì)象,代碼清單如下:
<?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>
在<Instrumentation>標(biāo)簽中,android:name聲明了測(cè)試框架,android:targetPackage指定了待測(cè)項(xiàng)目包名。
下面來(lái)看一下,如何用Instrumentation框架編寫(xiě)測(cè)試程序,代碼如下:
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<HelloBugbenActivity>這個(gè)類(lèi)。接著在setUp()方法中通過(guò)getActivity()方法獲取待測(cè)項(xiàng)目的實(shí)例,并通過(guò)textview1和textview2獲取兩個(gè)TextView控件。最后編寫(xiě)3個(gè)測(cè)試用例:控制文本設(shè)置測(cè)試testSetText()、字體加粗屬性測(cè)試testSetBold、字體大小屬性測(cè)試testSetSize()。這里用到的關(guān)鍵方法是Instrumentation API里面的getActivity()方法,待測(cè)的Activity在沒(méi)有調(diào)用此方法的時(shí)候是不會(huì)啟動(dòng)的。
眼尖的小伙伴可能已經(jīng)發(fā)現(xiàn)控制文本設(shè)置測(cè)試這里啟用了一個(gè)新線程,這是因?yàn)樵贏ndroid中相關(guān)的view和控件不是線程安全的,必須單獨(dú)在新的線程中做處理,不然會(huì)報(bào)
android.view.ViewRootImpl$CalledFromWrongThreadException:
Only the original thread that created a view hierarchy can touch its views
這個(gè)錯(cuò)誤。所以需要啟動(dòng)新線程進(jìn)行處理,具體步驟如下:
- 在setUp()方法中創(chuàng)建Handler對(duì)象,代碼如下:
public void setUp() throws Exception{
super.setUp();
handler = new Handler();
}
- 創(chuàng)建Runnable對(duì)象,在Runnable中進(jìn)行控件文本設(shè)置,代碼如下:
Runnable runnableTxt = new Runnable() {
@Override
public void run(){
// TODO Auto-generated method stub
helloBugben.setTxt(bugben_txt);
}
};
- 在具體測(cè)試方法中通過(guò)調(diào)用runnable對(duì)象,實(shí)現(xiàn)文本設(shè)置,代碼如下:
new Thread(){
public void run() {
if (handler != null) {
handler.post(runnableTxt);
}
}
}.start();
我們運(yùn)行一下結(jié)果,結(jié)果截圖如下:
可以看到3個(gè)測(cè)試用例結(jié)果運(yùn)行正常。
可能有小伙伴要問(wèn),程序中為啥要繼承ActivityInstrumentationTestCase2呢?我們先看一下ActivityInstrumentationTestCase2的繼承結(jié)構(gòu):
java.lang.Object
junit.framework.Assert
junit.framework.TestCase
android.test.InstrumentationTestCase
android.test.ActivityTestCase
android.test.ActivityInstrumentationTestCase2<T>
ActivityInstrumentationTestCase2允許InstrumentationTestCase. launchActivity來(lái)啟動(dòng)被測(cè)試的Activity。而且ActivityInstrumentationTestCase2還支持在新的UI線程中運(yùn)行測(cè)試方法,能注入Intent對(duì)象到被測(cè)試的Activity中,這樣一來(lái),我們就能直接操作被測(cè)試的Activity了。正因?yàn)锳ctivityInstrumentationTestCase2有如此出眾的有點(diǎn),它才成功取代了比它早出世的哥哥:ActivityInstrumentationTestCase,成為了Instrumentation測(cè)試的基礎(chǔ)。
Instrumentation測(cè)試框架實(shí)戰(zhàn)
了解完Instrumentation的基本測(cè)試方法后,我們來(lái)看一下如何運(yùn)用Instrumentation框架完成前文Monkey父子完成的自動(dòng)化測(cè)試任務(wù)。
- 首先建立項(xiàng)目名為Bugben的Project,類(lèi)名為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);
}
});
}
}
- 在建立一個(gè)名為OtherActivity的類(lèi),點(diǎn)擊提交按鈕后,跳轉(zhuǎn)到這個(gè)界面,代碼如下:
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.接下來(lái)需創(chuàng)建一個(gè)測(cè)試項(xiàng)目,命名為BugbenTestBase,選擇要測(cè)試的目標(biāo)項(xiàng)目為Bugben項(xiàng)目,然后點(diǎn)擊Finish即可完成測(cè)試項(xiàng)目的創(chuàng)建。
在com.ringo.bugben.test包中添加BugbenTestBase這個(gè)類(lèi),類(lèi)的代碼如下:
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 = "自動(dòng)化測(cè)試";
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();
// 啟動(dòng)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);
// 通過(guò)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();
}
// 提交測(cè)試
public void testSubmit()throws Throwable{
Log.v("Ringo", "test normal submit");
// 添加一個(gè)監(jiān)聽(tīng)器,監(jiān)視OtherActivity的啟動(dòng)
ActivityMonitor bugbenMonitor = getInstrumentation().addMonitor(
OtherActivity.class.getName(), null, false);
// 要操作待測(cè)程序的UI必須在runTestOnUiThread中執(zhí)行
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毫秒,避免程序響應(yīng)慢出錯(cuò)
SystemClock.sleep(500);
// 點(diǎn)擊提交按鈕
subButton.performClick();
}
});
// 從ActivityMonitor監(jiān)視器中獲取OtherActivity的實(shí)例
otherActivity = (OtherActivity)getInstrumentation().waitForMonitor(bugbenMonitor);
// 獲取的OtherActivity實(shí)例應(yīng)不為空
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毫秒,避免程序響應(yīng)慢出錯(cuò)
SystemClock.sleep(5000);
}
}
上述代碼中,共包括自動(dòng)化測(cè)試需要進(jìn)行的5個(gè)步驟,具體如下:
(1) 啟動(dòng)應(yīng)用:通過(guò)Intent對(duì)象setClassName()方法設(shè)置包名和類(lèi)名,通過(guò)setFlags()方法設(shè)置標(biāo)示,然后通過(guò)getInstrumentation()的startActivitySync(intent)來(lái)啟動(dòng)應(yīng)用,進(jìn)入到主界面。
(2) 編輯控件:在Android中相關(guān)的view和控件不是線程安全的,所以必須單獨(dú)在新的線程中做處理。代碼中我們?cè)趓unTestOnUiThread(new Runnable())中的run()方法中執(zhí)行的。
(3) 提交結(jié)果:點(diǎn)擊提交按鈕進(jìn)行結(jié)果的提交,由于點(diǎn)擊按鈕也屬于界面操作,所以也需要在runTestOnUiThread這個(gè)線程中完成。
(4) 界面跳轉(zhuǎn):這是Instrumentation自動(dòng)化測(cè)試中最需要注意的一個(gè)點(diǎn),特別是如何確認(rèn)界面已經(jīng)發(fā)生了跳轉(zhuǎn)。在Instrumentation中可以通過(guò)設(shè)置Monitor監(jiān)視器來(lái)確認(rèn)。代碼如下:
ActivityMonitor bugbenMonitor = getInstrumentation().addMonitor(
OtherActivity.class.getName(), null, false);
然后通過(guò)waitForMonitor方法等待界面跳轉(zhuǎn)。
otherActivity = (OtherActivity)getInstrumentation().waitForMonitor(bugbenMonitor);
若返回結(jié)果otherActivity對(duì)象不為空,說(shuō)明跳轉(zhuǎn)正常。
(5) 驗(yàn)證顯示:跳轉(zhuǎn)后,通過(guò)assertEquals()或assertTrue()方法來(lái)判斷顯示的正確性。
我們運(yùn)行一下結(jié)果,結(jié)果截圖如下:
Instrumentation工具總結(jié)
Instrumentation框架的整體運(yùn)行流程圖如下:
Instrumentation是基于源碼進(jìn)行腳本開(kāi)發(fā)的,測(cè)試的穩(wěn)定性好,可移植性高。正因?yàn)樗腔谠创a的,所以需要腳本開(kāi)發(fā)人員對(duì)Java語(yǔ)言、Android框架運(yùn)行機(jī)制、Eclipse開(kāi)發(fā)工具都非常熟悉。Instrumentation框架本身不支持多應(yīng)用的交互,例如測(cè)試“通過(guò)短信中的號(hào)碼去撥打電話”這個(gè)用例,被測(cè)應(yīng)用將從短信應(yīng)用界面跳轉(zhuǎn)到撥號(hào)應(yīng)用界面,但I(xiàn)nstrumentation沒(méi)有辦法同事控制短信和撥號(hào)兩個(gè)應(yīng)用,這是因?yàn)锳ndroid系統(tǒng)自身的安全性限制,禁止多應(yīng)用的進(jìn)程間相互訪問(wèn)。
法寶4:終極自動(dòng)化測(cè)試框架——UIAutomator
鑒于Instrumentation框架需要讀懂項(xiàng)目源碼、腳本開(kāi)發(fā)難度較高并且不支持多應(yīng)用交互,Android官網(wǎng)亮出了自動(dòng)化測(cè)試的王牌——UIAutomator,并主推這個(gè)自動(dòng)化測(cè)試框架。該框架無(wú)需項(xiàng)目源碼,腳本開(kāi)發(fā)效率高且難度低,并且支持多應(yīng)用的交互。當(dāng)UIAutomator面世后,Instrumentation框架回歸到了其單元測(cè)試框架的本來(lái)位置。
下面我們來(lái)看一下這個(gè)框架是如何運(yùn)行起來(lái)的。首先運(yùn)行位于Android SDK的tools目錄下的uiautomatorviewer.bat,可以看到啟動(dòng)界面。
啟動(dòng)bugben應(yīng)用后,點(diǎn)擊
這個(gè)圖標(biāo)來(lái)采集手機(jī)的界面信息,如下所示:
我們可以看到,用uiautomatorviewer捕捉到的控件非常清晰,很方便元素位置的定位。在UIAutomator框架中,測(cè)試程序與待測(cè)程序之間是松耦合關(guān)系,即完全不需要獲取待測(cè)程序的控件ID,只需對(duì)控件的文本(text)、描述(content-desc)等信息進(jìn)行識(shí)別即可。
在進(jìn)行實(shí)戰(zhàn)之前,我們先看一下UIAutomator的API部分,由以下架構(gòu)圖組成。
下面來(lái)看下如何利用該框架創(chuàng)建測(cè)試工程。
- 創(chuàng)建BugBenTestUIAuto項(xiàng)目,右鍵點(diǎn)擊項(xiàng)目并選擇Properties > Java Build Path
點(diǎn)擊Add Library > Junit > Junit3,添加Junit框架。
點(diǎn)擊Add External Jar,并導(dǎo)航到Android SDK目錄下,選擇platforms目錄下面的android.jar和UIAutomator.jar兩個(gè)文件。
- 設(shè)置完成后,可以開(kāi)始編寫(xiě)項(xiàng)目測(cè)試的代碼,具體如下:
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();
// 啟動(dòng)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();
}
// 提交文字測(cè)試
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");
}
// 獲取加粗選項(xiàng)并賦值 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 加粗");
}
// 獲取大號(hào)字體選項(xiàng)并賦值
UiObject bugben_big = new UiObject(new UiSelector().text("大號(hào)"));
if(bugben_big.exists() && bugben_big.isEnabled()){
bugben_big.click();
}else{
Log.e("Ringo", "can not find 大號(hào)"); }
// 獲取提交按鈕并跳轉(zhuǎn)
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)));
// 驗(yàn)證
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類(lèi),并讓BugbenTest繼承自UiAutomatorTestCase這個(gè)類(lèi)。同樣,我們來(lái)看下UiAutomator框架下自動(dòng)化測(cè)試進(jìn)行的5個(gè)步驟,具體如下:
(1) 啟動(dòng)應(yīng)用:于Instrumentation框架不同,UiAutomator是通過(guò)命令行進(jìn)行應(yīng)用啟動(dòng)的。
am start –n 包名/.應(yīng)用名
(2) 編輯控件:UiAutomator框架中,控件的編輯相對(duì)簡(jiǎn)單,直接通過(guò)UiSelector的text()方法找到對(duì)應(yīng)的控件,然后調(diào)用控件的setText()即可對(duì)其賦值。
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) 提交結(jié)果:點(diǎn)擊提交按鈕進(jìn)行結(jié)果的提交,也是通過(guò)UiSelector的text()方法找到對(duì)應(yīng)的控件,然后調(diào)用clickAndWaitForNewWindow()方法來(lái)等待跳轉(zhuǎn)完成。
UiObject subButton = new UiObject(new UiSelector().text("提交"));
if(subButton.exists() && subButton.isEnabled()){
subButton.clickAndWaitForNewWindow();
}
(4) 界面跳轉(zhuǎn)元素獲取:用uiautomatorviewer捕捉跳轉(zhuǎn)后的控件,例如捕捉跳轉(zhuǎn)后的文本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) 驗(yàn)證顯示:跳轉(zhuǎn)后,通過(guò)assertEquals()或assertTrue()方法來(lái)判斷顯示的正確性。
if (bugben_tv1.exists() && bugben_tv1.isEnabled())
{assertEquals(bugben_txt1, bugben_tv1.getText().toString());}
至此核心代碼部分已編寫(xiě)完畢。UIAutomator有一個(gè)麻煩之處:沒(méi)法通過(guò)Eclipse直接編譯。可以借助于一系列命令行進(jìn)行編譯,詳細(xì)步驟如下:
- 通過(guò)如下命令創(chuàng)建編譯的build.xml文件
<android-sdk目錄>/tools/android create uitest-project –n 工程名 –t 1 –p 項(xiàng)目路徑
針對(duì)我們的項(xiàng)目,命令如下:
android create uitest-project –n BugBenTestUIAuto –t 1 –p "E:\workspace\BugBenTestUIAuto"
創(chuàng)建完成后,刷新BugBenTestUIAuto項(xiàng)目,得到如下圖:
打開(kāi)build.xml會(huì)看到,編譯項(xiàng)目名為BugBenTestUIAuto。
- 設(shè)置SDK的路徑:
set ANDROID_HOME="E:\sdk\android-sdk-windows"
- 進(jìn)入測(cè)試目錄,然后進(jìn)行編譯:
cd /d E:\workspace\android\BugBenTestUIAutoant build
編譯完成后,再次刷新項(xiàng)目,你將看到BugBenTestUIAuto.jar包生成在bin目錄下了,如圖:
- 將生成的jar包推送到手機(jī)端
adb push E:\workspace\android\BugBenTestUIAuto\bin\BugBenTestUIAuto.jar /data/local/tmp/
- 在手機(jī)端運(yùn)行自動(dòng)化腳本,即jar包中的測(cè)試用例,命令行如下:
adb shell uiautomator runtest BugBenTestUIAuto.jar -c com.ringo.bugben.test.BugBenTest
運(yùn)行結(jié)果如下,返回OK表示運(yùn)行成功。
- 最后,將運(yùn)行后的截圖從手機(jī)端拷貝到PC上
adb pull /data/local/tmp/displayCheck.png E:\workspace\android\BugBenTestUIAuto
至此整個(gè)代碼就編譯和運(yùn)行完畢,如果覺(jué)得調(diào)試時(shí)反復(fù)修改和編譯比較麻煩,可以將以上腳本寫(xiě)成一個(gè)批處理文件。
UIAutomator工具總結(jié)
相比于Instrumentation工具,UIAutomator工具更靈活一些,它不需要項(xiàng)目源碼,擁有可視化的界面和可視化的樹(shù)狀層級(jí)列表,極大降低了自動(dòng)化測(cè)試腳本開(kāi)發(fā)的門(mén)檻。并且UIAutomator支持多應(yīng)用的交互,彌補(bǔ)了Instrumentation工具的不足。但UIAutomator難以捕捉到控件的顏色、字體粗細(xì)、字號(hào)等信息,要驗(yàn)證該類(lèi)信息的話,需要通過(guò)截圖的方式進(jìn)行半自動(dòng)驗(yàn)證。同時(shí),UIAutomator的調(diào)試相比Instrumentation要困難。所以在平時(shí)的測(cè)試過(guò)程中,建議將兩者結(jié)合起來(lái)使用,可達(dá)到更佳的效果!
注:核心內(nèi)容轉(zhuǎn)自許奔的《深入理解Android自動(dòng)化測(cè)試》
關(guān)于騰訊WeTest (wetest.qq.com)
騰訊WeTest是騰訊游戲官方推出的一站式游戲測(cè)試平臺(tái),用十年騰訊游戲測(cè)試經(jīng)驗(yàn)幫助廣大開(kāi)發(fā)者對(duì)游戲開(kāi)發(fā)全生命周期進(jìn)行質(zhì)量保障。騰訊WeTest提供:適配兼容測(cè)試;云端真機(jī)調(diào)試;安全測(cè)試;耗電量測(cè)試;服務(wù)器性能測(cè)試;輿情監(jiān)控等服務(wù)。