Android自動化測試技術——Espresso的使用

配置

修改設置

先啓用開發者選項,再在開發者選項下,停用如下三項設置:html

  • 窗口動畫縮放
  • 過渡動畫縮放
  • Animator 時長縮放

添加依賴

app/build.gradle文件中添加依賴java

androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
複製代碼

app/build.gradle文件中的android.defaultConfig中添加android

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
複製代碼

注意:上面的依賴只能實現基本功能,若是你想使用全部的功能,則按下面的配置:git

全部依賴github

androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.ext:truth:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test:rules:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'
複製代碼

下面調用的方法如onView()等都是靜態方法,能夠經過import static XXX來直接調用,全部須要導入的靜態方法以下:api

import static androidx.test.espresso.Espresso.*;
import static androidx.test.espresso.action.ViewActions.*;
import static androidx.test.espresso.assertion.ViewAssertions.*;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.ComponentNameMatchers.*;
import static androidx.test.espresso.intent.matcher.IntentMatchers.*;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static androidx.test.ext.truth.content.IntentSubject.assertThat;
複製代碼

Api組件

經常使用Api組件包括:bash

  • Espresso - 用於與視圖交互(經過 onView() 和 onData())的入口點。此外,還公開不必定與任何視圖相關聯的 API,如 pressBack()。
  • ViewMatchers - 實現 Matcher<? super View> 接口的對象的集合。您能夠將其中一個或多個對象傳遞給 onView() 方法,以在當前視圖層次結構中找到某個視圖。
  • ViewActions - 可傳遞給 ViewInteraction.perform() 方法的 ViewAction 對象(如 click())的集合。
  • ViewAssertions - 可傳遞給 ViewInteraction.check() 方法的 ViewAssertion 對象的集合。在大多數狀況下,您將使用 matches 斷言,它使用視圖匹配器斷言當前選定視圖的狀態。

大多數可用的 Matcher、ViewAction 和 ViewAssertion 實例以下圖(來源官方文檔):app

經常使用的api實例pdf框架

使用

普通控件

示例:MainActivity 包含一個 Button 和一個 TextView。點擊該按鈕後,TextView 的內容會變爲 "改變成功"dom

使用 Espresso 進行測試方法以下:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextTest {
   @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);
    @Test
    public void test_change_text(){
        onView(withId(R.id.change))
                .perform(click());
        onView(withId(R.id.content))
              .check(matches(withText("改變成功")));
    }
}    
複製代碼

onView()方法用來獲取匹配的當前視圖,注意匹配的視圖只能有一個,不然會報錯。

withId()方法用來搜索匹配的視圖,相似的還有withText()withHint()等。

perform()方法用來執行某種操做,例如點擊click() 、長按longClick() 、雙擊doubleClick()

check()用來將斷言應用於當前選定的視圖

matches()最經常使用的斷言,它斷言當前選定視圖的狀態。上面的示例就是斷言id爲content的View它是否和text爲"改變成功"的View匹配

AdapterView相關控件

與普通控件不一樣,AdapterView(經常使用的是ListView)只能將一部分子視圖加載到當前視圖層次結構中。簡單的 onView() 搜索將找不到當前未加載的視圖。Espresso 提供一個單獨的 onData() 入口點,該入口點可以先加載相關適配器項目,並在對其或其任何子級執行操做以前使其處於聚焦狀態。

示例:打開Spinner,選擇一個特定的條目,而後驗證 TextView 是否包含該條目。Spinner 會建立一個包含其內容的 ListView,所以須要onData()

@RunWith(AndroidJUnit4.class)
@LargeTest
public class SpinnerTest {
   @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);
    @Test
    public void test_spinner(){
        String content = "學校";
        //點擊Spnner,顯示項目
        onView(withId(R.id.change)).perform(click());
        //點擊指定的內容
        onData(allOf(is(instanceOf(String.class)), is(content))).perform(click());
        //判斷TextView是否包含指定內容
        onView(withId(R.id.content))
                .check(matches(withText(containsString(content))));
    }
}

複製代碼

下圖爲AdapterView的繼承關係圖:

警告:若是 AdapterView 的自定義實現違反繼承約定,那麼在使用 onData() 方法(尤爲是 getItem() API)時可能會出現問題。在這種狀況下,最好的作法是重構應用代碼。若是您沒法執行此操做,則能夠實現匹配的自定義 AdapterViewProtocol。

自定義Matcher和ViewAction

在介紹RecyclerView的操做以前,咱們先要看看如何自定義MatcherViewAction

自定義Matcher

Matcher<T>是一個用來匹配視圖的接口,經常使用的是它的兩個實現類BoundedMatcher<T, S extends T>TypeSafeMatcher<T>

BoundedMatcher<T, S extends T>:一些匹配的語法糖,可讓你建立一個給定的類型,而匹配的特定亞型的只有過程項匹配。 類型參數:<T> - 匹配器的指望類型。<S> - T的亞型

TypeSafeMatcher<T>:內部實現了空檢查,檢查的類型,而後進行轉換

示例:輸入EditText值,若是值以000開頭,則讓內容爲 "成功" 的TextView可見,不然讓內容爲 失敗 的TextView可見.

@RunWith(AndroidJUnit4.class)
@LargeTest
public class EditTextTest {
   @Rule
    public ActivityTestRule<MainActivity> activityRule =
            new ActivityTestRule<>(MainActivity.class);

   @Test
    public void rightInput() {
        onView(withId(R.id.editText))
                .check(matches(EditMatcher.isRight()))
                .perform(typeText("000123"), ViewActions.closeSoftKeyboard());
        onView(withId(R.id.button)).perform(click());
        onView(withId(R.id.textView_success)).check(matches(isDisplayed()));
        onView(withId(R.id.textView_fail)).check(matches(not(isDisplayed())));
    }

    @Test
    public void errorInput() {
        onView(withId(R.id.editText))
                .check(matches(EditMatcher.isRight()))
                .perform(typeText("003"), ViewActions.closeSoftKeyboard());
        onView(withId(R.id.button)).perform(click());
        onView(withId(R.id.textView_success)).check(matches(not(isDisplayed())));
        onView(withId(R.id.textView_fail)).check(matches(isDisplayed()));
    }
       
   static class EditMatcher{
       static Matcher<View> isRight(){
           //自定義Matcher
           return new BoundedMatcher<View, EditText>(EditText.class) {
               @Override
               public void describeTo(Description description) {
                     description.appendText("EditText不知足要求");
               }

               @Override
               protected boolean matchesSafely(EditText item) {
                  //在輸入EditText以前,先判EditText是否可見以及hint是否爲指定值
                   if (item.getVisibility() == View.VISIBLE &&
                   item.getText().toString().isEmpty())
                       return true;
                   else
                   return false;
               }
           };
       }
   }
}   
複製代碼

自定義ViewAction

這個不太熟悉,這裏就介紹一下實現ViewAction接口,要實現的方法的做用

/** *符合某種限制的視圖 */
  public Matcher<View> getConstraints();

  /** *返回視圖操做的描述。 *說明不該該過長,應該很好地適應於一句話 */
  public String getDescription();

  /** * 執行給定的視圖這個動做。 *PARAMS:uiController - 控制器使用與UI交互。 *view - 在採起行動的view。 不能爲null */
  public void perform(UiController uiController, View view);
}
複製代碼

RecyclerView

RecyclerView 對象的工做方式與 AdapterView 對象不一樣,所以不能使用 onData() 方法與其交互。 要使用 EspressoRecyclerView 交互,您可使用 espresso-contrib 軟件包,該軟件包具備 RecyclerViewActions的集合,定義了用於滾動到相應位置或對項目執行操做的方法。

添加依賴

androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
複製代碼

操做RecyclerView的方法有:

  • scrollTo() - 滾動到匹配的視圖。
  • scrollToHolder() - 滾動到匹配的視圖持有者。
  • scrollToPosition() - 滾動到特定位置。
  • actionOnHolderItem() - 對匹配的視圖持有者執行視圖操做。
  • actionOnItem() - 對匹配的視圖執行視圖操做。
  • actionOnItemAtPosition() - 在特定位置對視圖執行視圖操做。

示例:選中刪除功能:點擊 編輯 ,TextView內容轉爲 刪除 ,同時RecycleView的條目出現選中框,勾選要刪除的項,點擊 刪除 ,刪除指定項,RecycleView的條目的選中框消失。

@RunWith(AndroidJUnit4.class)
@LargeTest
public class RecyclerViewTest {
   @Rule
    public ActivityTestRule<RecyclerActivity> activityRule =
            new ActivityTestRule<>(RecyclerActivity.class);
   

    static class ClickCheckBoxAction implements ViewAction{
        
        @Override
        public Matcher<View> getConstraints() {
            return any(View.class);
        }

        @Override
        public String getDescription() {
            return null;
        }

        @Override
        public void perform(UiController uiController, View view) {
            CheckBox box = view.findViewById(R.id.checkbox);
            box.performClick();//點擊
        }
    }
    
    static class MatcherDataAction implements ViewAction{
        
        private String require;

        public MatcherDataAction(String require) {
            this.require = require;
        }

        @Override
        public Matcher<View> getConstraints() {
            return any(View.class);
        }

        @Override
        public String getDescription() {
            return null;
        }

        @Override
        public void perform(UiController uiController, View view) {
            TextView text = view.findViewById(R.id.text);
            assertThat("數據值不匹配",require,equalTo(text.getText().toString()));
        }
    }
    
    public void delete_require_data(){
        //獲取RecyclerView中顯示的全部數據
        List<String> l = new ArrayList<>(activityRule.getActivity().getData());
        //點擊 編輯 ,判斷text是否變成 刪除
        onView(withId(R.id.edit))
                .perform(click())
                .check(matches(withText("刪除")));
        //用來記錄要刪除的項,
        Random random = new Random();
        int time = random.nextInt(COUNT);
        List<String> data = new ArrayList<>(COUNT);
        for (int i = 0; i < COUNT; i++) {
            data.add("");
        }
        for (int i = 0; i < time; i++) {
            //隨機生成要刪除的位置
            int position = random.nextInt(COUNT);
            //因爲再次點擊會取消,這裏用來記錄最後肯定要刪除的項
            if (data.get(position).equals(""))
                data.set(position,"測試數據"+position);
            else data.set(position,"");
            //調用RecyclerViewActions.actionOnItemAtPosition()方法,滑到指定位置
            //在執行指定操做
           onView(withId(R.id.recycler)).
                  perform(RecyclerViewActions.actionOnItemAtPosition(position,new ClickCheckBoxAction()));
        }
        //點擊 刪除 ,判斷text是否變成 編輯
        onView(withId(R.id.edit))
                .perform(click(),doubleClick())
                .check(matches(withText("編輯")));
        //刪除無用的項
        data.removeIf(s -> s.equals(""));
        //獲取最後保存的項
        l.removeAll(data);
        //依次判斷保留的項是否還存在
        for (int i = 0; i < l.size(); i++) {
            final String require = l.get(i);
            onView(withId(R.id.recycler))
                    .perform(RecyclerViewActions.
                            actionOnItemAtPosition(i,new MatcherDataAction(require)));
        }
    }
}
複製代碼

注意:在MatcherDataAction中調用了assertThat(),這種方式是不建議的。這裏是我沒有找到更好的方式來實現這個測試。

Intent

Espresso-Intents 是 Espresso 的擴展,支持對被測應用發出的 Intent 進行驗證和打樁。

添加依賴:

androidTestImplementation 'androidx.test.ext:truth:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0'
複製代碼

在編寫 Espresso-Intents 測試以前,要先設置 IntentsTestRule。這是 ActivityTestRule 類的擴展,可以讓您在功能界面測試中輕鬆使用 Espresso-Intents的API。IntentsTestRule 會在帶有 @Test 註解的每一個測試運行前初始化Espresso-Intents,並在每一個測試運行後釋放 Espresso-Intents

@Rule
 public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
            MainActivity.class);
複製代碼

驗證 Intent

示例:在EditText中,輸入電話號碼,點擊撥打按鍵,撥打電話。

@RunWith(AndroidJUnit4.class)
@LargeTest
public class IntentTest {
   
    //設置撥打電話的權限的環境
    @Rule
    public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant("android.permission.CALL_PHONE");

    @Rule
    public IntentsTestRule<MainActivity> mActivityRule = new IntentsTestRule<>(
            MainActivity.class);

    @Test
    public void test_start_other_app_intent(){
         String phoneNumber = "123456";
         //輸入電話號碼
         onView(withId(R.id.phone))
                 .perform(typeText(phoneNumber), ViewActions.closeSoftKeyboard());
        //點擊撥打
         onView(withId(R.id.button))
                 .perform(click());
         //驗證Intent是否正確
         intended(allOf(
                hasAction(Intent.ACTION_CALL),
                hasData(Uri.parse("tel:"+phoneNumber))));
    }
}
複製代碼

intended():是Espresso-Intents 提供的用來驗證Intent的方法

除此以外,還能夠經過斷言的方式來驗證Intent

Intent receivedIntent = Iterables.getOnlyElement(Intents.getIntents());
assertThat(receivedIntent)
      .extras()
      .string("phone")
      .isEqualTo(phoneNumber);
複製代碼

插樁

上述方式能夠解決通常的Intent驗證的操做,可是當咱們須要調用startActivityForResult()方法去啓動照相機獲取照片時,若是使用通常的方式,咱們就須要手動去點擊拍照,這樣就不算自動化測試了。

Espresso-Intents 提供了intending()方法來解決這個問題,它能夠爲使用 startActivityForResult() 啓動的 Activity 提供樁響應。簡單來講就是,它不會去啓動照相機,而是返回你本身定義的Intent。

@RunWith(AndroidJUnit4.class)
@LargeTest
public class TakePictureTest {
       
        public static BoundedMatcher<View, ImageView> hasDrawable() {
            return new BoundedMatcher<View, ImageView>(ImageView.class) {
                @Override
                public void describeTo(Description description) {
                    description.appendText("has drawable");
                }

                @Override
                public boolean matchesSafely(ImageView imageView) {
                    return imageView.getDrawable() != null;
                }
            };
        }
        
    @Rule
    public IntentsTestRule<MainActivity> mIntentsRule = new IntentsTestRule<>(
            MainActivity.class);

    @Rule
    public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant(Manifest.permission.CAMERA);

    @Before
    public void stubCameraIntent() {
        Instrumentation.ActivityResult result = createImageCaptureActivityResultStub();
        intending(hasAction(MediaStore.ACTION_IMAGE_CAPTURE)).respondWith(result);
    }

    @Test
    public void takePhoto_drawableIsApplied() {
        //先檢查ImageView中是否已經設置了圖片
        onView(withId(R.id.image)).check(matches(not(hasDrawable())));
        // 點擊拍照
        onView(withId(R.id.button)).perform(click());
        // 判斷ImageView中是否已經設置了圖片
        onView(withId(R.id.image)).check(matches(hasDrawable()));
    }

    private Instrumentation.ActivityResult createImageCaptureActivityResultStub() {
        //本身定義Intent
        Bundle bundle = new Bundle();
        bundle.putParcelable("data", BitmapFactory.decodeResource(
                mIntentsRule.getActivity().getResources(), R.drawable.ic_launcher_round));
        Intent resultData = new Intent();
        resultData.putExtras(bundle);
        return new Instrumentation.ActivityResult(Activity.RESULT_OK, resultData);
    }
}
複製代碼

空閒資源

空閒資源表示結果會影響界面測試中後續操做的異步操做。經過向 Espresso 註冊空閒資源,能夠在測試應用時更可靠地驗證這些異步操做。

添加依賴

implementation 'androidx.test.espresso:espresso-idling-resource:3.2.0'
複製代碼

下面以Google的官方示例來介紹,如何使用:

第一步:建立SimpleIdlingResource類,用來實現IdlingResource

public class SimpleIdlingResource implements IdlingResource {

    @Nullable
    private volatile ResourceCallback mCallback;

    private AtomicBoolean mIsIdleNow = new AtomicBoolean(true);

    @Override
    public String getName() {
        return this.getClass().getName();
    }

    /** *false 表示這裏有正在進行的任務,而true表示異步任務完成 */
    @Override
    public boolean isIdleNow() {
        return mIsIdleNow.get();
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
        mCallback = callback;
    }

    public void setIdleState(boolean isIdleNow) {
        mIsIdleNow.set(isIdleNow);
        if (isIdleNow && mCallback != null) {
           //調用這個方法後,Espresso不會再檢查isIdleNow()的狀態,直接判斷異步任務完成
            mCallback.onTransitionToIdle();
        }
    }
}
複製代碼

第二步:建立執行異步任務的類MessageDelayer

class MessageDelayer {

    private static final int DELAY_MILLIS = 3000;

    interface DelayerCallback {
        void onDone(String text);
    }

    static void processMessage(final String message, final DelayerCallback callback, @Nullable final SimpleIdlingResource idlingResource) {
        if (idlingResource != null) {
            idlingResource.setIdleState(false);
        }
        Handler handler = new Handler();
        new Thread(()->{
            try {
                Thread.sleep(DELAY_MILLIS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            handler.post(new Runnable() {
                @Override
                public void run() {
                    if (callback != null) {
                        callback.onDone(message);
                        if (idlingResource != null) {
                            idlingResource.setIdleState(true);
                        }
                    }
                }
            });
        }).start();
    }
}
複製代碼

第三步:在MainActivity中經過點擊按鈕開啓任務

public class MainActivity extends AppCompatActivity implements View.OnClickListener, MessageDelayer.DelayerCallback {

    private TextView mTextView;
    private EditText mEditText;

    @Nullable
    private SimpleIdlingResource mIdlingResource;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.changeTextBt).setOnClickListener(this);
        mTextView = findViewById(R.id.textToBeChanged);
        mEditText = findViewById(R.id.editTextUserInput);
    }

    @Override
    public void onClick(View view) {
        final String text = mEditText.getText().toString();
        if (view.getId() == R.id.changeTextBt) {
            mTextView.setText("正在等待");
            MessageDelayer.processMessage(text, this, mIdlingResource);
        }
    }

    @Override
    public void onDone(String text) {
        mTextView.setText(text);
    }

    /** * 僅測試能調用,建立並返回新的SimpleIdlingResource */
    @VisibleForTesting
    @NonNull
    public IdlingResource getIdlingResource() {
        if (mIdlingResource == null) {
            mIdlingResource = new SimpleIdlingResource();
        }
        return mIdlingResource;
    }
}
複製代碼

第四步:建立測試用例

@RunWith(AndroidJUnit4.class)
@LargeTest
public class ChangeTextBehaviorTest {

    private static final String STRING_TO_BE_TYPED = "Espresso";

    private IdlingResource mIdlingResource;


    /** *註冊IdlingResource實例 */
    @Before
    public void registerIdlingResource() {
        ActivityScenario activityScenario = ActivityScenario.launch(MainActivity.class);
        activityScenario.onActivity((ActivityScenario.ActivityAction<MainActivity>) activity -> {
            mIdlingResource = activity.getIdlingResource();
            IdlingRegistry.getInstance().register(mIdlingResource);
        });
    }

    @Test
    public void changeText_sameActivity() {
        onView(withId(R.id.editTextUserInput))
                .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
        onView(withId(R.id.changeTextBt)).perform(click());
        //只須要註冊IdlingResource實例,Espresso就會自動在這裏等待,直到異步任務完成
        //在執行下面的代碼
        onView(withId(R.id.textToBeChanged)).check(matches(withText(STRING_TO_BE_TYPED)));
    }
    //取消註冊
    @After
    public void unregisterIdlingResource() {
        if (mIdlingResource != null) {
            IdlingRegistry.getInstance().unregister(mIdlingResource);
        }
    }
}
複製代碼

不足:Espresso提供了一套先進的同步功能。不過,該框架的這一特性僅適用於在 MessageQueue 上發佈消息的操做,如在屏幕上繪製內容的 View 子類。

其餘

Espresso還有在多進程、WebView、無障礙功能檢查、多窗口等內容,這些我不太熟悉,建議本身看 安卓官方文檔或者下面的官方示例。

官方示例

參考

相關文章
相關標籤/搜索