在上一篇博客筆者介紹了Espresso的基礎(chǔ)用法,在文章最后拋出了一個(gè)問(wèn)題,簡(jiǎn)短的說(shuō)就是異步的情況下,如何保證測(cè)試的正確進(jìn)行。
如果沒(méi)有看過(guò)的,建議先看這一篇,傳送門在這里:
搬磚之余來(lái)一杯意式濃縮咖啡(Espresso基本用法)
那么開始這篇博客的正題了
- 明確問(wèn)題
- 解決方案
- 優(yōu)雅的實(shí)現(xiàn)方式
- 實(shí)例演示
明確問(wèn)題
在很多時(shí)候,我們都會(huì)進(jìn)行網(wǎng)絡(luò)請(qǐng)求,當(dāng)進(jìn)行網(wǎng)絡(luò)請(qǐng)求的時(shí)候,由于網(wǎng)絡(luò)的原因,我們不確定它什么時(shí)候可以返回給我結(jié)果。如果還是直接用上節(jié)的測(cè)試方法,很大概率會(huì)出現(xiàn)問(wèn)題,因?yàn)闇y(cè)試代碼是無(wú)腦順序執(zhí)行的,不知道什么時(shí)候它該停下來(lái)等待網(wǎng)絡(luò)請(qǐng)求。
你或許會(huì)想到一個(gè)騷操作:在測(cè)試的時(shí)候,我在請(qǐng)求網(wǎng)絡(luò)的時(shí)候讓它睡個(gè)幾秒(幾秒你還不請(qǐng)求完成?),然后在繼續(xù)執(zhí)行測(cè)試代碼。哈哈,這波操作還是很騷的,但是會(huì)遇到一個(gè)問(wèn)題:你還是不確定這個(gè)等待時(shí)間是多少;如果睡時(shí)間短了,還是會(huì)測(cè)試錯(cuò)誤,如果睡時(shí)間長(zhǎng)了,就會(huì)浪費(fèi)等待時(shí)間。所以,這個(gè)騷操作還是不可取的......有風(fēng)險(xiǎn)啊
那么該怎么辦???選擇狗帶?
解決方案
既然Espresso是Google爸爸推崇的UI自動(dòng)化測(cè)試工具,這個(gè)問(wèn)題肯定有解決方法的。從上面的問(wèn)題我們可以知道問(wèn)題的根本原因就是我們不知道它什么時(shí)候完成網(wǎng)絡(luò)請(qǐng)求。準(zhǔn)確的說(shuō)是異步操作的完成。
在這個(gè)基礎(chǔ)上,Google給我們提供了IdlingResource這樣一個(gè)接口
,通過(guò)這個(gè)接口,在我們測(cè)試的Activity中實(shí)現(xiàn)這個(gè)接口,通過(guò)里面的回調(diào)方法在通知測(cè)試類,我的異步操作完成了,你可以繼續(xù)你的下一步測(cè)試了。
public interface IdlingResource {
/**
* 用來(lái)標(biāo)識(shí) IdlingResource 名稱
*/
public String getName();
/**
* 當(dāng)前 IdlingResource 是否空閑 .
*/
public boolean isIdleNow();
/**
注冊(cè)一個(gè)空閑狀態(tài)變換的ResourceCallback回調(diào)
*/
public void registerIdleTransitionCallback(ResourceCallback callback);
/**
* 通知Espresso當(dāng)前IdlingResource狀態(tài)變換為空閑的回調(diào)接口
*/
public interface ResourceCallback {
/**
* 當(dāng)前狀態(tài)轉(zhuǎn)變?yōu)榭臻e時(shí),調(diào)用該方法告訴Espresso
*/
public void onTransitionToIdle();
}
}
哇,看似好牛逼啊,但是這樣的話我需要測(cè)試的每個(gè)Activity都要實(shí)現(xiàn)這個(gè)接口,還要實(shí)現(xiàn)這么多方法,多繁瑣啊。會(huì)出現(xiàn)好多冗余的代碼。在Activity添加代碼是肯定要的了,但是我們可以減少冗余的代碼量。以一個(gè)優(yōu)雅的方式去實(shí)現(xiàn)。
優(yōu)雅的實(shí)現(xiàn)方式
在使用IdlingResource之前,我們要添加兩個(gè)庫(kù)
implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
注意第一個(gè)庫(kù)要用implementation而不是androidTestImplementation,因?yàn)槲覀円跍y(cè)試代碼的外面使用IdlingResource,使用androidTestImplementation會(huì)找不到這個(gè)類,編譯就不能通過(guò)。
接下來(lái)我們創(chuàng)建一個(gè)類實(shí)現(xiàn)IdlingResource接口
public class SimpleCountingIdlingResource implements IdlingResource {
private final String mResourceName;
//這個(gè)counter值就像一個(gè)標(biāo)記,默認(rèn)為0
private final AtomicInteger counter = new AtomicInteger(0);
private volatile ResourceCallback resourceCallback;
public SimpleCountingIdlingResource(String resourceNme){
mResourceName=resourceNme;
}
@Override
public String getName() {
return mResourceName;
}
@Override
public boolean isIdleNow() {
return counter.get()==0;
}
@Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
resourceCallback=callback;
}
//每當(dāng)我們開始異步請(qǐng)求,把counter值+1
public void increment(){
counter.getAndIncrement();
}
//當(dāng)我們獲取到網(wǎng)絡(luò)數(shù)據(jù)后,counter值-1
public void decrement(){
int counterVal=counter.decrementAndGet();
//如果counterVal的值為0說(shuō)明異步結(jié)束,執(zhí)行回調(diào)
if(counterVal==0){
if(resourceCallback!=null){
resourceCallback.onTransitionToIdle();
}
}
if(counterVal<0)
//如果小于0,拋出異常
throw new IllegalArgumentException("Counter has been corrupted!");
}
}
這個(gè)類定義了一個(gè)標(biāo)記counter,通過(guò)這個(gè)標(biāo)記的值,來(lái)判斷何時(shí)接口回調(diào),從而測(cè)試類可以知道這個(gè)時(shí)候它的異步任務(wù)完成了,這時(shí)候就可以繼續(xù)進(jìn)行下一步的測(cè)試。
但是SimpleCountingIdlingResource這個(gè)類看起來(lái)還是有點(diǎn)雜亂的,我們?cè)儆靡粋€(gè)管理類來(lái)封裝它,讓它處理業(yè)務(wù)部分。
ublic class EspressoIdlingResource {
private static final String RESOURCE = "GLOBAL";
private static SimpleCountingIdlingResource mCountingIdlingResource =
new SimpleCountingIdlingResource(RESOURCE);
public static void increment(){
mCountingIdlingResource.increment();
}
public static void decrement(){
mCountingIdlingResource.decrement();
}
public static IdlingResource getIdlingResource(){
return mCountingIdlingResource;
}
}
所以最終我們只需要直接使用EspressoIdlingResource這個(gè)類就行了。
說(shuō)這么多還是太抽象了,下面用一個(gè)實(shí)例來(lái)感受一下。
實(shí)例演示
還是用之前的登錄來(lái)進(jìn)行測(cè)試,不過(guò)添加了一個(gè)線程睡眠來(lái)模擬一個(gè)網(wǎng)絡(luò)請(qǐng)求的等待時(shí)間。
MainActivity.class
public class MainActivity extends AppCompatActivity {
EditText edName;
EditText edPass;
Button btClick;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btClick=(Button)findViewById(R.id.bt_click);
edName=(EditText) findViewById(R.id.ed_username);
edPass=(EditText) findViewById(R.id.ed_pass);
btClick.setText("登錄");
}
public void clickButton(View view){
btClick.setText("登錄中...");
MyThread myThread=new MyThread();
myThread.start();
}
class MyThread extends Thread{
@Override
public void run() {
super.run();
try {
Thread.sleep(5000); //讓該線程睡眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
if(edName.getText().toString().equals("jasonking")&&edPass.getText().toString().equals("123")){
runOnUiThread(new Runnable() {
@Override
public void run() {
btClick.setText("登錄成功");
}
});
}else{
runOnUiThread(new Runnable() {
@Override
public void run() {
btClick.setText("登錄失敗");
}
});
}
}
}
}
如果我們繼續(xù)用之前的測(cè)試用例,運(yùn)行測(cè)試會(huì)發(fā)現(xiàn),測(cè)試不能通過(guò)。因?yàn)槲覀兤谂蔚氖恰暗卿洺晒Α?,但?s內(nèi),我們得到的結(jié)果是“登錄中...”,只有5秒之后才可能返回"登錄成功。
接下來(lái),我們就可以使用之前準(zhǔn)備的工具類了,對(duì)這個(gè)Activity進(jìn)行標(biāo)記
異步開始前的標(biāo)記
public void clickButton(View view){
btClick.setText("登錄中...");
//在開始異步請(qǐng)求前添加這行代碼,意味著開始了異步
EspressoIdlingResource.increment();
MyThread myThread=new MyThread();
myThread.start();
}
異步結(jié)束后的標(biāo)記
class MyThread extends Thread{
@Override
public void run() {
//省略...
//異步結(jié)束的時(shí)候,添加這行代碼
if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) {
EspressoIdlingResource.decrement();
}
}
}
添加這個(gè)方法,獲取這個(gè)類的標(biāo)識(shí)
@VisibleForTesting
public IdlingResource getCountingIdlingResource() {
return EspressoIdlingResource.getIdlingResource();
}
最后再修改一下測(cè)試類
MyEspressoAsyncTest.class
相比較之前的,這里多做了3個(gè)步驟
- 獲取需要測(cè)試的Activity的標(biāo)識(shí),之后為了添加到異步監(jiān)聽集合中
- 注冊(cè)異步監(jiān)聽
- 在測(cè)試結(jié)束后取消注冊(cè),釋放資源
@RunWith(AndroidJUnit4.class)
@LargeTest
public class MyEspressoAsyncTest {
@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);
private IdlingResource idlingResource;
@Before
public void setUp() throws Exception{
//獲取這個(gè)類的標(biāo)識(shí)
idlingResource=mActivityRule.getActivity().getCountingIdlingResource();
}
@Test
public void onLoadingFinished(){
//清空文本框,然后輸入用戶名jasonking,關(guān)閉軟鍵盤
onView(withId(R.id.ed_username))
.perform(
clearText(),
replaceText("jasonking"),
closeSoftKeyboard()
)
.check(matches(withText("jasonking")));
//清空文本框,然后輸入密碼,關(guān)閉軟鍵盤
onView(withId(R.id.ed_pass))
.perform(
clearText(),
replaceText("123"),
closeSoftKeyboard()
)
.check(matches(withText("123")));
//點(diǎn)擊按鈕檢查文本是不是登錄
onView(withId(R.id.bt_click))
.check(matches(withText("登錄")))
.perform(click());
//注冊(cè)異步監(jiān)聽,,此時(shí)測(cè)試會(huì)掛起,等待網(wǎng)絡(luò)請(qǐng)求結(jié)束后,繼續(xù)測(cè)試
IdlingRegistry.getInstance().register(idlingResource);
Log.d(TAG, "setUp: "+"請(qǐng)求網(wǎng)絡(luò)請(qǐng)求完成");
//繼續(xù)執(zhí)行代碼
onView(withId(R.id.bt_click))
.check(matches(withText("登錄成功")));
}
@After
public void release() throws Exception {
// 當(dāng)然,我們需要在測(cè)試結(jié)束后取消注冊(cè),釋放資源
IdlingRegistry.getInstance().unregister(idlingResource);
}
}
運(yùn)行測(cè)試可以看到結(jié)果是pass的
