在上一篇博客筆者介紹了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(){
return mCountingIdlingResource;
}
}
所以最終我們只需要直接使用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的