手把手教你Android標(biāo)準(zhǔn)App的四大自動(dòng)化測(cè)試法寶

作者: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)證提交后的效果。

![](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可以編寫(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è)試方法。

  1. 首先建立項(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è)置不同的文本格式。

  1. 對(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)建。
![](http://upload-images.jianshu.io/upload_images/1944350-a36c366acd98b643.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

可以注意到,該項(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)行處理,具體步驟如下:

  1. 在setUp()方法中創(chuàng)建Handler對(duì)象,代碼如下:
public void setUp() throws Exception{           
 super.setUp();            
 handler = new Handler(); 
   }
  1. 創(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);     
  }  
    };
  1. 在具體測(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ù)。

  1. 首先建立項(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);        
     }           
  });   
 }
}
  1. 在建立一個(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è)試工程。

  1. 創(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è)文件。

  1. 設(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ì)步驟如下:

  1. 通過(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。

  1. 設(shè)置SDK的路徑:
set ANDROID_HOME="E:\sdk\android-sdk-windows"
  1. 進(jìn)入測(cè)試目錄,然后進(jìn)行編譯:
cd /d E:\workspace\android\BugBenTestUIAutoant build

編譯完成后,再次刷新項(xiàng)目,你將看到BugBenTestUIAuto.jar包生成在bin目錄下了,如圖:

  1. 將生成的jar包推送到手機(jī)端
adb push E:\workspace\android\BugBenTestUIAuto\bin\BugBenTestUIAuto.jar /data/local/tmp/
  1. 在手機(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)行成功。

  1. 最后,將運(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ù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評(píng)論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,947評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,094評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,485評(píng)論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,268評(píng)論 6 405
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,817評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,039評(píng)論 0 285
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,551評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,502評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,662評(píng)論 1 366
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,907評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,304評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,563評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,255評(píng)論 3 389
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,637評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,576評(píng)論 25 707
  • 解放程序猿寶貴的右手(或者是左手) ——Android自動(dòng)化測(cè)試技巧 Google大神鎮(zhèn)樓 : http://de...
    eclipse_xu閱讀 4,958評(píng)論 6 40
  • 標(biāo)簽(空格分隔): Android 單元測(cè)試的好處:Martin Fowler在《重構(gòu)》里面還解釋了為什么單元測(cè)試...
    背影殺手不太冷閱讀 5,836評(píng)論 3 25
  • Instrumentation介紹 Instrumentation是個(gè)什么東西? Instrumentation測(cè)...
    打不死的小強(qiáng)qz閱讀 7,810評(píng)論 2 39
  • 我以前常說(shuō)我要去漂泊 我喜歡漂泊 而在漂泊過(guò)的這兩年里我逐漸愛(ài)上了鄉(xiāng)下 我想有一片地 當(dāng)然最好面向一面海 或者一汪...
    soumnstan閱讀 387評(píng)論 0 1