搬磚之餘來一杯意式濃縮咖啡(Espresso高階用法)
在上一篇部落格筆者介紹了Espresso的基礎用法,在文章最後丟擲了一個問題,簡短的說就是 非同步的情況下,如何保證測試的正確進行。
如果沒有看過的,建議先看這一篇,傳送門在這裡:
ofollow,noindex">搬磚之餘來一杯意式濃縮咖啡(Espresso基本用法)
那麼開始這篇部落格的正題了
- 明確問題
- 解決方案
- 優雅的實現方式
- 例項演示
明確問題
在很多時候,我們都會進行網路請求,當進行網路請求的時候,由於網路的原因,我們不確定它什麼時候可以返回給我結果。如果還是直接用上節的測試方法,很大概率會出現問題,因為測試程式碼是無腦順序執行的,不知道什麼時候它該停下來等待網路請求。
你或許會想到一個騷操作:在測試的時候,我在請求網路的時候讓它睡個幾秒(幾秒你還不請求完成?),然後在繼續執行測試程式碼。哈哈,這波操作還是很騷的,但是會遇到一個問題:你還是不確定這個等待時間是多少;如果睡時間短了,還是會測試錯誤,如果睡時間長了,就會浪費等待時間。所以,這個騷操作還是不可取的......有風險啊
那麼該怎麼辦???選擇狗帶?
解決方案
既然Espresso是Google爸爸推崇的UI自動化測試工具,這個問題肯定有解決方法的。從上面的問題我們可以知道問題的根本原因就是我們不知道它什麼時候完成網路請求。準確的說是非同步操作的完成。
在這個基礎上,Google給我們提供了 IdlingResource這樣一個介面
,通過這個介面,在我們測試的Activity中實現這個介面,通過裡面的回撥方法在通知測試類,我的非同步操作完成了,你可以繼續你的下一步測試了。
public interface IdlingResource { /** * 用來標識 IdlingResource 名稱 */ public String getName(); /** * 當前 IdlingResource 是否空閒 . */ public boolean isIdleNow(); /** 註冊一個空閒狀態變換的ResourceCallback回撥 */ public void registerIdleTransitionCallback(ResourceCallback callback); /** * 通知Espresso當前IdlingResource狀態變換為空閒的回撥介面 */ public interface ResourceCallback { /** * 當前狀態轉變為空閒時,呼叫該方法告訴Espresso */ public void onTransitionToIdle(); } }
哇,看似好牛逼啊,但是這樣的話我需要測試的每個Activity都要實現這個介面,還要實現這麼多方法,多繁瑣啊。會出現好多冗餘的程式碼。在Activity新增程式碼是肯定要的了,但是我們可以減少冗餘的程式碼量。以一個優雅的方式去實現。
優雅的實現方式
在使用IdlingResource之前,我們要新增兩個庫
implementation 'com.android.support.test.espresso:espresso-idling-resource:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
注意第一個庫要用implementation而不是androidTestImplementation,因為我們要在測試程式碼的外面使用IdlingResource,使用androidTestImplementation會找不到這個類,編譯就不能通過。
接下來我們建立一個類實現IdlingResource介面
public class SimpleCountingIdlingResource implements IdlingResource { private final String mResourceName; //這個counter值就像一個標記,預設為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; } //每當我們開始非同步請求,把counter值+1 public void increment(){ counter.getAndIncrement(); } //當我們獲取到網路資料後,counter值-1 public void decrement(){ int counterVal=counter.decrementAndGet(); //如果counterVal的值為0說明非同步結束,執行回撥 if(counterVal==0){ if(resourceCallback!=null){ resourceCallback.onTransitionToIdle(); } } if(counterVal<0) //如果小於0,丟擲異常 throw new IllegalArgumentException("Counter has been corrupted!"); } }
這個類定義了一個標記counter,通過這個標記的值,來判斷何時介面回撥,從而測試類可以知道這個時候它的非同步任務完成了,這時候就可以繼續進行下一步的測試。
但是SimpleCountingIdlingResource這個類看起來還是有點雜亂的,我們再用一個管理類來封裝它,讓它處理業務部分。
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(){ returnmCountingIdlingResource; } }
所以最終我們只需要直接使用EspressoIdlingResource這個類就行了。
說這麼多還是太抽象了,下面用一個例項來感受一下。
例項演示
還是用之前的登入來進行測試,不過添加了一個執行緒睡眠來模擬一個網路請求的等待時間。
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("登入失敗"); } }); } } } }
如果我們繼續用之前的測試用例,執行測試會發現,測試不能通過。因為我們期盼的是“登入成功”,但是5s內,我們得到的結果是“登入中...”,只有5秒之後才可能返回"登入成功。
接下來,我們就可以使用之前準備的工具類了,對這個Activity進行標記
非同步開始前的標記
public void clickButton(View view){ btClick.setText("登入中..."); //在開始非同步請求前新增這行程式碼,意味著開始了非同步 EspressoIdlingResource.increment(); MyThread myThread=new MyThread(); myThread.start(); }
非同步結束後的標記
class MyThread extends Thread{ @Override public void run() { //省略... //非同步結束的時候,新增這行程式碼 if (!EspressoIdlingResource.getIdlingResource().isIdleNow()) { EspressoIdlingResource.decrement(); } } }
新增這個方法,獲取這個類的標識
@VisibleForTesting public IdlingResource getCountingIdlingResource() { return EspressoIdlingResource.getIdlingResource(); }
最後再修改一下測試類
MyEspressoAsyncTest.class
相比較之前的,這裡多做了3個步驟
- 獲取需要測試的Activity的標識,之後為了新增到非同步監聽集合中
- 註冊非同步監聽
- 在測試結束後取消註冊,釋放資源
@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{ //獲取這個類的標識 idlingResource=mActivityRule.getActivity().getCountingIdlingResource(); } @Test public void onLoadingFinished(){ //清空文字框,然後輸入使用者名稱jasonking,關閉軟鍵盤 onView(withId(R.id.ed_username)) .perform( clearText(), replaceText("jasonking"), closeSoftKeyboard() ) .check(matches(withText("jasonking"))); //清空文字框,然後輸入密碼,關閉軟鍵盤 onView(withId(R.id.ed_pass)) .perform( clearText(), replaceText("123"), closeSoftKeyboard() ) .check(matches(withText("123"))); //點選按鈕檢查文字是不是登入 onView(withId(R.id.bt_click)) .check(matches(withText("登入"))) .perform(click()); //註冊非同步監聽,,此時測試會掛起,等待網路請求結束後,繼續測試 IdlingRegistry.getInstance().register(idlingResource); Log.d(TAG, "setUp: "+"請求網路請求完成"); //繼續執行程式碼 onView(withId(R.id.bt_click)) .check(matches(withText("登入成功"))); } @After public void release() throws Exception { // 當然,我們需要在測試結束後取消註冊,釋放資源 IdlingRegistry.getInstance().unregister(idlingResource); } }
執行測試可以看到結果是pass的

testpass.png