搬磚之余來(lái)一杯意式濃縮咖啡(Espresso高級(jí)用法)

在上一篇博客筆者介紹了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的

testpass.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容