先啓用開發者選項,再在開發者選項下,停用如下三項設置:html
在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組件包括:bash
大多數可用的 Matcher、ViewAction 和 ViewAssertion 實例以下圖(來源官方文檔):app
示例: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
(經常使用的是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。
在介紹RecyclerView
的操做以前,咱們先要看看如何自定義Matcher
和ViewAction
。
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
對象的工做方式與 AdapterView
對象不一樣,所以不能使用 onData()
方法與其交互。 要使用 Espresso
與 RecyclerView
交互,您可使用 espresso-contrib
軟件包,該軟件包具備 RecyclerViewActions
的集合,定義了用於滾動到相應位置或對項目執行操做的方法。
添加依賴
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.2.0'
複製代碼
操做RecyclerView
的方法有:
示例:選中刪除功能:點擊 編輯 ,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()
,這種方式是不建議的。這裏是我沒有找到更好的方式來實現這個測試。
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);
複製代碼
示例:在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、無障礙功能檢查、多窗口等內容,這些我不太熟悉,建議本身看 安卓官方文檔或者下面的官方示例。