作者:Ringoyan温技,騰訊測(cè)試開發(fā)工程師上真。先后為植物大戰(zhàn)僵尸Online祭埂,糖果傳奇等游戲擔(dān)任測(cè)試經(jīng)理绞蹦,其負(fù)責(zé)的“我叫MT2”測(cè)試項(xiàng)目曾獲騰訊互動(dòng)娛樂精品文化獎(jiǎng)銀獎(jiǎng)。目前擔(dān)任騰訊WeTest測(cè)試經(jīng)理嘀倒。擅長領(lǐng)域:App的自動(dòng)化測(cè)試和Web的安全測(cè)試工作。
注:核心內(nèi)容轉(zhuǎn)自許奔的《深入理解Android自動(dòng)化測(cè)試》,本書將在許奔公眾號(hào)“巴哥奔”中全文連載废恋。
商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系騰訊WeTest獲得授權(quán)谈秫,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
WeTest導(dǎo)讀
說起Android的自動(dòng)化測(cè)試鱼鼓,相信有很多小伙伴都接觸過或者有所耳聞拟烫,本文從框架最基本的功能介紹及API的使用入手,結(jié)合簡單的項(xiàng)目實(shí)戰(zhàn)來幫忙大家對(duì)該框架進(jìn)一步理解和加深印象迄本。下面讓我們來一睹標(biāo)準(zhǔn)App的四大自動(dòng)化測(cè)試法寶的風(fēng)采硕淑!
法寶1:穩(wěn)定性測(cè)試?yán)鳌狹onkey
要想發(fā)布一個(gè)新版本,得先通過穩(wěn)定性測(cè)試嘉赎。理想情況是找個(gè)上幼兒園的弟弟妹妹置媳,打開應(yīng)用把手機(jī)交給他,讓他胡亂的玩公条,看你的程序能不能接受這樣的折騰拇囊。但是我們身邊不可能都有正太和蘿莉,也不能保證他們拿到手機(jī)后不是測(cè)試軟件的健壯性靶橱,反而測(cè)試你的手機(jī)經(jīng)不經(jīng)摔寥袭,這與我們的期望差太遠(yuǎn)了…
Google公司考慮到我們的需要,開發(fā)出了Monkey這個(gè)工具关霸。但在很多人的印象中传黄,Monkey測(cè)試就是讓設(shè)備隨機(jī)的亂點(diǎn),事件都是隨機(jī)產(chǎn)生的队寇,不帶任何人的主觀性膘掰。很少有人知道,其實(shí)Monkey也可以用來做簡單的自動(dòng)化測(cè)試工作佳遣。
Mokey基本功能介紹
首先识埋,介紹下Monkey的基本使用,如果要發(fā)送500個(gè)隨機(jī)事件零渐,只需運(yùn)行如下命令:
adb shell monkey 500
插上手機(jī)運(yùn)行后窒舟,大家是不是發(fā)現(xiàn)手機(jī)開始瘋狂的運(yùn)行起來了。So Easy!
在感受完Monkey的效果后相恃,發(fā)現(xiàn)這“悟空”太調(diào)皮了辜纲,根本招架不住啊拦耐!是否有類似“緊箍咒”這種約束類命令耕腾,讓這只猴子在某個(gè)包或類中運(yùn)行呢?要想Monkey牢牢的限制在某個(gè)包中杀糯,命令也很簡單:
adb shell monkey –p your-package-name 500
-p后面接你程序的包名扫俺。多想限制在多個(gè)包中,可以在命令行中添加多個(gè)包:
adb shell monkey –p your-package1-name –p your-package2-name 500
這樣“悟空”就飛不出你的五指山了固翰。
Mokey編寫自動(dòng)化測(cè)試腳本
若控制不住“悟空”狼纬,只讓它隨機(jī)亂點(diǎn)的話羹呵,Monkey是替代不了黑盒測(cè)試用例的。我們能不能想些辦法疗琉,控制住“悟空”讓他做些簡單的自動(dòng)化測(cè)試的工作呢冈欢?下面來看一下,如何用Monkey來編寫腳本盈简。
先簡單介紹下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) 長按鍵值:LongPress(int keyCode)
(8) 發(fā)送鍵值:DispatchKey(參數(shù)1~參數(shù)8)
(9) 打開軟鍵盤:DispatchFlip(Boolean keyboardOpen)
了解完常用API后香浩,我們來看一下Monkey腳本的編寫規(guī)范。Monkey Script是按照一定的語法規(guī)則編寫的有序的用戶事件流臼勉,使用于Monkey命令工具的腳本邻吭。Monkey腳本一般以如下4條語句開頭:
# Start Script
type = user #指明腳本類型
count = 10 #腳本執(zhí)行次數(shù)
speed = 1.0 #命令執(zhí)行速率
start data >> #用戶腳本入口,下面是用戶自己編寫的腳本```
下面來看一個(gè)簡單應(yīng)用的實(shí)戰(zhàn)宴霸,實(shí)現(xiàn)的效果很簡單囱晴,就是隨便輸入文本,選擇選項(xiàng)再進(jìn)行提交猖败,提交后要驗(yàn)證提交后的效果速缆。
![](http://upload-images.jianshu.io/upload_images/1944350-04000f883b90cd4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
![](http://upload-images.jianshu.io/upload_images/1944350-258fbd5d09060de5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
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可以編寫腳本做簡單的自動(dòng)化測(cè)試幢尚,但局限性非常大,例如無法進(jìn)行截屏操作翅楼,不能簡單的支持插件的編寫尉剩,沒有好的辦法控制事件流,不支持錄制回放等毅臊。我們?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工具無法滿足的,這里給大家推薦另一款工具M(jìn)onkeyRunner德迹。
同樣先簡單的介紹下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編寫自動(dòng)化測(cè)試腳本
下面我們來看下蹬竖,用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目錄下阴绢,就可以通過如下代碼進(jìn)行啟動(dòng):
monkeyrunner monkey_recorder.py
運(yùn)行結(jié)果如下圖所示:
下面用MonkeyRecorder提供的控件,來進(jìn)行腳本的錄制艰躺。
錄制完成后呻袭,導(dǎo)出腳本保存為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',}
腳本錄制完畢腺兴,接來下看看回放腳本是否正常左电。回放腳本時(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類似,也是基于控件坐標(biāo)進(jìn)行定位的盼玄,這樣的定位方式極易導(dǎo)致回放失敗贴彼。
法寶3:單元測(cè)試框架——Instrumentation
Monkey父子均可通過編寫相應(yīng)的腳本,在不依賴源碼的前提下完成部分自動(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)行失敗青灼。怎樣可以不依賴坐標(biāo)來進(jìn)行應(yīng)用的自動(dòng)化測(cè)試呢暴心?下面就要亮出自動(dòng)化測(cè)試的屠龍寶刀了——Instrumentation框架。
Instrumentation框架主要是依靠控件的ID來進(jìn)行定位的杂拨,擁有成熟的用例管理系統(tǒng)专普,是Android主推的白盒測(cè)試框架。若想對(duì)項(xiàng)目進(jìn)行深入的弹沽、系統(tǒng)的單元測(cè)試檀夹,基本上都離不開Instrumentation這把屠龍寶刀。
在了解Instrumentation框架之前策橘,先對(duì)Android組件生命周期對(duì)應(yīng)的回調(diào)函數(shù)做個(gè)說明:
從上圖可以看出炸渡,Activity處于不同狀態(tài)時(shí),將調(diào)用不同的回調(diào)函數(shù)丽已。但Android API不提供直接調(diào)用這些回調(diào)函數(shù)的方法蚌堵,在Instrumentation中則可以這樣做。Instrumentation類通過“hooks”控制著Android組件的正常生命周期沛婴,同時(shí)控制Android系統(tǒng)加載應(yīng)用程序吼畏。通過Instrumentation類我們可以在測(cè)試代碼中調(diào)用這些回調(diào)函數(shù),就像在調(diào)試該控件一樣一步一步地進(jìn)入到該控件的整個(gè)生命周期中嘁灯。
Instrumentation和Activity有點(diǎn)類似泻蚊,只不過Activity是需要一個(gè)界面的,而Instrumentation并不是這樣的丑婿,我們可以將它理解為一種沒有圖形界面的性雄,具有啟動(dòng)能力的,用于監(jiān)控其他類(用Target Package聲明)的工具類枯冈。
下面通過一個(gè)簡單的例子來講解Instrumentation的基本測(cè)試方法毅贮。
- 首先建立項(xiàng)目名為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);
}
}
這個(gè)程序的功能很簡單,就是給2個(gè)TextView的內(nèi)容設(shè)置不同的文本格式病蛉。
- 對(duì)于測(cè)試工程師而言炫加,HelloBugben是一個(gè)已完成的項(xiàng)目。接下來需創(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)簽沽瘦,這就說明該測(cè)試項(xiàng)目是針對(duì)HelloBugben所設(shè)置的革骨。
打開AndroidManifest可看到<Instrumentation>標(biāo)簽农尖,該標(biāo)簽元素用來指定要測(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)目包名筑凫。
下面來看一下滑沧,如何用Instrumentation框架編寫測(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è)類。接著在setUp()方法中通過getActivity()方法獲取待測(cè)項(xiàng)目的實(shí)例棚潦,并通過textview1和textview2獲取兩個(gè)TextView控件殖属。最后編寫3個(gè)測(cè)試用例:控制文本設(shè)置測(cè)試testSetText()、字體加粗屬性測(cè)試testSetBold瓦盛、字體大小屬性測(cè)試testSetSize()洗显。這里用到的關(guān)鍵方法是Instrumentation API里面的getActivity()方法,待測(cè)的Activity在沒有調(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è)試方法中通過調(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)行正常耻蛇。
可能有小伙伴要問踪蹬,程序中為啥要繼承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來啟動(dòng)被測(cè)試的Activity臣咖。而且ActivityInstrumentationTestCase2還支持在新的UI線程中運(yùn)行測(cè)試方法跃捣,能注入Intent對(duì)象到被測(cè)試的Activity中,這樣一來夺蛇,我們就能直接操作被測(cè)試的Activity了疚漆。正因?yàn)锳ctivityInstrumentationTestCase2有如此出眾的有點(diǎn),它才成功取代了比它早出世的哥哥:ActivityInstrumentationTestCase,成為了Instrumentation測(cè)試的基礎(chǔ)娶聘。
Instrumentation測(cè)試框架實(shí)戰(zhàn)
了解完Instrumentation的基本測(cè)試方法后灵临,我們來看一下如何運(yùn)用Instrumentation框架完成前文Monkey父子完成的自動(dòng)化測(cè)試任務(wù)。
- 首先建立項(xiàng)目名為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);
}
});
}
}
- 在建立一個(gè)名為OtherActivity的類,點(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.接下來需創(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è)類孵淘,類的代碼如下:
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);
// 通過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)聽器蒲障,監(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)用:通過Intent對(duì)象setClassName()方法設(shè)置包名和類名背捌,通過setFlags()方法設(shè)置標(biāo)示毙籽,然后通過getInstrumentation()的startActivitySync(intent)來啟動(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中可以通過設(shè)置Monitor監(jiān)視器來確認(rèn)熊泵。代碼如下:
ActivityMonitor bugbenMonitor = getInstrumentation().addMonitor(
OtherActivity.class.getName(), null, false);
然后通過waitForMonitor方法等待界面跳轉(zhuǎn)仰迁。
otherActivity = (OtherActivity)getInstrumentation().waitForMonitor(bugbenMonitor);
若返回結(jié)果otherActivity對(duì)象不為空,說明跳轉(zhuǎn)正常顽分。
(5) 驗(yàn)證顯示:跳轉(zhuǎn)后,通過assertEquals()或assertTrue()方法來判斷顯示的正確性施蜜。
我們運(yùn)行一下結(jié)果卒蘸,結(jié)果截圖如下:
Instrumentation工具總結(jié)
Instrumentation框架的整體運(yùn)行流程圖如下:
Instrumentation是基于源碼進(jìn)行腳本開發(fā)的,測(cè)試的穩(wěn)定性好,可移植性高缸沃。正因?yàn)樗腔谠创a的恰起,所以需要腳本開發(fā)人員對(duì)Java語言、Android框架運(yùn)行機(jī)制趾牧、Eclipse開發(fā)工具都非常熟悉检盼。Instrumentation框架本身不支持多應(yīng)用的交互,例如測(cè)試“通過短信中的號(hào)碼去撥打電話”這個(gè)用例翘单,被測(cè)應(yīng)用將從短信應(yīng)用界面跳轉(zhuǎn)到撥號(hào)應(yīng)用界面吨枉,但I(xiàn)nstrumentation沒有辦法同事控制短信和撥號(hào)兩個(gè)應(yīng)用,這是因?yàn)锳ndroid系統(tǒng)自身的安全性限制哄芜,禁止多應(yīng)用的進(jìn)程間相互訪問貌亭。
法寶4:終極自動(dòng)化測(cè)試框架——UIAutomator
鑒于Instrumentation框架需要讀懂項(xiàng)目源碼、腳本開發(fā)難度較高并且不支持多應(yīng)用交互认臊,Android官網(wǎng)亮出了自動(dòng)化測(cè)試的王牌——UIAutomator圃庭,并主推這個(gè)自動(dòng)化測(cè)試框架。該框架無需項(xiàng)目源碼失晴,腳本開發(fā)效率高且難度低剧腻,并且支持多應(yīng)用的交互。當(dāng)UIAutomator面世后涂屁,Instrumentation框架回歸到了其單元測(cè)試框架的本來位置恕酸。
下面我們來看一下這個(gè)框架是如何運(yùn)行起來的。首先運(yùn)行位于Android SDK的tools目錄下的uiautomatorviewer.bat胯陋,可以看到啟動(dòng)界面蕊温。
啟動(dòng)bugben應(yīng)用后,點(diǎn)擊
這個(gè)圖標(biāo)來采集手機(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)圖組成。
下面來看下如何利用該框架創(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è)置完成后骇钦,可以開始編寫項(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類眯搭,并讓BugbenTest繼承自UiAutomatorTestCase這個(gè)類窥翩。同樣,我們來看下UiAutomator框架下自動(dòng)化測(cè)試進(jìn)行的5個(gè)步驟坦仍,具體如下:
(1) 啟動(dòng)應(yīng)用:于Instrumentation框架不同鳍烁,UiAutomator是通過命令行進(jìn)行應(yīng)用啟動(dòng)的。
am start –n 包名/.應(yīng)用名
(2) 編輯控件:UiAutomator框架中繁扎,控件的編輯相對(duì)簡單幔荒,直接通過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é)果的提交爹梁,也是通過UiSelector的text()方法找到對(duì)應(yīng)的控件,然后調(diào)用clickAndWaitForNewWindow()方法來等待跳轉(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)后盼忌,通過assertEquals()或assertTrue()方法來判斷顯示的正確性积糯。
if (bugben_tv1.exists() && bugben_tv1.isEnabled())
{assertEquals(bugben_txt1, bugben_tv1.getText().toString());}
至此核心代碼部分已編寫完畢。UIAutomator有一個(gè)麻煩之處:沒法通過Eclipse直接編譯谦纱】闯桑可以借助于一系列命令行進(jìn)行編譯,詳細(xì)步驟如下:
- 通過如下命令創(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)目,得到如下圖:
打開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)行完畢,如果覺得調(diào)試時(shí)反復(fù)修改和編譯比較麻煩馍盟,可以將以上腳本寫成一個(gè)批處理文件于置。
UIAutomator工具總結(jié)
相比于Instrumentation工具,UIAutomator工具更靈活一些贞岭,它不需要項(xiàng)目源碼八毯,擁有可視化的界面和可視化的樹狀層級(jí)列表,極大降低了自動(dòng)化測(cè)試腳本開發(fā)的門檻瞄桨。并且UIAutomator支持多應(yīng)用的交互话速,彌補(bǔ)了Instrumentation工具的不足。但UIAutomator難以捕捉到控件的顏色芯侥、字體粗細(xì)泊交、字號(hào)等信息,要驗(yàn)證該類信息的話柱查,需要通過截圖的方式進(jìn)行半自動(dòng)驗(yàn)證廓俭。同時(shí),UIAutomator的調(diào)試相比Instrumentation要困難唉工。所以在平時(shí)的測(cè)試過程中研乒,建議將兩者結(jié)合起來使用,可達(dá)到更佳的效果淋硝!
注:核心內(nèi)容轉(zhuǎn)自許奔的《深入理解Android自動(dòng)化測(cè)試》
關(guān)于騰訊WeTest (wetest.qq.com)
騰訊WeTest是騰訊游戲官方推出的一站式游戲測(cè)試平臺(tái)雹熬,用十年騰訊游戲測(cè)試經(jīng)驗(yàn)幫助廣大開發(fā)者對(duì)游戲開發(fā)全生命周期進(jìn)行質(zhì)量保障。騰訊WeTest提供:適配兼容測(cè)試谣膳;云端真機(jī)調(diào)試竿报;安全測(cè)試;耗電量測(cè)試参歹;服務(wù)器性能測(cè)試仰楚;輿情監(jiān)控等服務(wù)。