自動化測試之Espresso學習

1.爲了確保測試穩定性,使用前須要在開發者選項中關閉一下三個設置:
  • 窗口動畫縮放;
  • 過分動畫縮放;
  • Animator 時長縮放;
2.如何使用:
  • 添加必要的依賴:
// dependencies 下面
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation 'androidx.test:rules:1.1.0'

// android 的 defaultConfig 下面
// 添加 Instrumentation Runner
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
  • test runner 因爲須要確保每一個新版本都正常工做,因此會收集並上傳分析信息,這個能夠經過 adb 指令來手動關閉;
  • first test,須要建立在 src/androidTest/java/包名 下,以下:
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ExampleInstrumentedTest {

// 經過這個來指定在哪一個 Activity 下面查找
@Rule
public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = ApplicationProvider.getApplicationContext();

assertEquals("com.jaaaelu.gzw.autotest", appContext.getPackageName());
}

@Test
public void matchesText() {
// 去匹配是否存在對應的文字
// 精確匹配不然失敗
onView(withText("Hello")).check(matches(isDisplayed()));
}
}
  • 運行方式有兩種:
    • 在 Android Studio 中,Run -> Edit Configurations 添加一個可運行的測試配置,而後運行便可,這裏能夠指定模塊、測試範圍等等;
    • 也能夠經過指令行的方式運行;
  • 該框架阻止直接訪問應用程序的 Activities 和 views of the application,由於要保持這些對象並在 UI 線程上對他們進行操做是測試片斷的主要來源;
    • 因此不會提供 getView() getCurrentActivity() 這樣的方法;
    • 不過能夠經過 ViewAction 和 ViewAssertion 來安全操做那些 View;
3.主要組件介紹:
  • Espresso,與 View 交互的入口,主要是 onView 和 onData;
  • ViewMatchers,一個 Matcher<? super View> 的實例建立類,提供了各類 Matcher 的建立;
    • 能夠將一個或多個傳遞給 onView;
    • 咱們能夠自定義,繼承自 BaseMatcher 便可;
  • ViewAction,負責在給定的 View 上執行交互;
  • ViewAssertions,提供了經常使用的 ViewAssertion 實例;
    • 不過大多數時候咱們使用的是匹配斷言,它使用 View 的匹配器來斷言當前所選視圖的狀態;
  • ViewInteraction,爲開發人員提供主要接口,以便對 View 進行操做或斷言,不過這個咱們不直接操做 Espresso.onView 會返回這個對象;
實例:
@Test
public void clickFab() {
// 對 R.id.fab 執行點擊事件
onView(withId(R.id.fab)).perform(click()).check(matches(isDisplayed()));
}
4.經常使用功能介紹:
  • 尋找某個 View:
    • 若是所需 View 具備惟一 id,那麼直接使用 onView(withId( R.id.xxx)) 便可;
      • 補充,onView() 大多數是採用的是 hamcrest matchers,該匹配器在當前視圖層次結構中匹配一個且只能匹配一個視圖;
    • 若是一個 view hierarchy 中出現重複的 id 那麼上面那個就不起做用了,須要使用另外一種方式:
      • onView(allOf(withId(R.id.view), withText("Hello"))); 也就是經過其餘特徵找到對應的 View;
      • onView(allOf(withId(R.id.view), not(withText("Hello"))));
      • 補充,建議良好的應用程序中的 View 應該包含描述性文本或具體內容描述,不然沒法使用使用 withText() 或 withContentDescription(),這些方法能夠幫你縮小匹配範圍;
  • 對 View 執行操做:
    • 能夠執行一個或多個操做,好比常見的點擊事件,onView(withId(R.id.fab)).perform(click());
    • 多個操做點擊了輸入 Test 進去,onView(withId(R.id.et_input)).perform(typeText("Test"), click());
    • 若是在 ScrollView 中還能夠經過 scrollTo() 操做先滑動到對應位置,不過若是已經顯示在頁面上,就不起做用了;
    • 能夠同一個對一個無效的 id 進行操做,從而查看視圖結構,例如 onView(withId(-1)).perform(click());
  • View 斷言:
    • 使用 check 方法對當前 View 進行斷言,而 matches 方法表示具體斷言;
  • 檢查列表中的數據加載:
    • 經過 onData() 來處理這個問題;
    • 例如:onView(withText("Hello World!")).check(matches(isDisplayed())); 查看顯示對應文字的控價是否可見;
5.常見斷言與操做:
  • View 沒有顯示:onView(...).check(matcher(not(isDisplayed())));
  • View 不在視圖結構中:onView(...).check(doesNotExist());
  • 數據是否在適配器中,能夠經過自定義 Matcher 來完成;
  • 能夠經過自定義異常處理器來處理異常狀況,有點類似線程的異常處理器;
// 官方
private static class CustomFailureHandler implements FailureHandler {
private final FailureHandler delegate;

public CustomFailureHandler(Context targetContext) {
delegate = new DefaultFailureHandler(targetContext);
}

@Override
public void handle(Throwable error, Matcher<View> viewMatcher) {

try {
delegate.handle(error, viewMatcher);
} catch (NoMatchingViewException e) {
throw new MySpecialException(e);
}
}
}
  • 與非默認 window 交互,好比彈窗之類的,Espresso 會猜想您打算和哪一個窗口進行交互;
// 點開 Fab 按鈕,彈出 Dialog
onView(withId(R.id.fab)).perform(click());
// 而後點擊 Dialog 的 yes 按鈕,使 Dialog 消失
onView(withText("yes")).inRoot(withDecorView(not(is(activityTestRule.getActivity().getWindow().getDecorView())))).perform(click());
  • ListView 的頭尾也能夠匹配;
6.Espresso 能夠在多進程中的使用,
  • 注意事項:
    • 8.0 以上;
    • 沒法測試後應用程序外的進程;
  • 按照官網進行配置便可,不然會報錯的;
在 src/androidTest/AndroidManifest.xml 下面加上便可
<!-- 多進程必備 -->
<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.jaaaelu.gzw.autotest"
android:targetProcesses="*">
<meta-data
android:name="remoteMethod"
android:value="androidx.test.espresso.remote.EspressoRemote#remoteInit" />
</instrumentation>
  • 默認狀況下測試的是應用啓動的默認線程;
  • 每一個進程都有一個 AndroidJUintRunner 實例,每一個 AndroidJUintRunner 都把 Espresso 註冊爲測試框架,不一樣進程的 AndroidJUintRunner 須要經過握手來創建鏈接,圖片是跨進程通訊:
7.測試代碼可訪問性,經過使用 AccessibilityChecks 類來使用。
8.AndroidJUnitRunner 類是一個JUnit測試運行器,容許您在 Android 設備上運行 JUnit 3 或 JUnit 4 樣式的測試類,包括那些使用 Espresso 和 UI Automator 測試框架的測試類。測試運行器處理將測試包和被測應用程序加載到設備,運行測試和報告測試結果。此類替換了 InstrumentationTestRunner 類,該類僅支持 JUnit 3 測試。

頁面依賴或者數據依賴怎麼解決?
可否記錄流程,好比截圖
onData

9.onData 用來匹配數據(onView 也能夠匹配 Text,而 onData 更可能是和那種 AdapterView 的控件一塊兒使用,好比 ListView、Spinner 等),onView 用來匹配試圖,例如:
// 咱們經過 onData 匹配列表的內容,並給出對應 Id
onData(withItemContent("item: 60"))
.onChildView(withId(R.id.item_size))
.perform(click());
對應匹配到的內容如圖,先經過 item: 60 找到內容對應的行,而後再經過提供的控件 Id 去找到對應控件(不過這種用法通常適用於 ListView 這樣的控件):

若是咱們使用的是 RecycleView 的話,就須要配合 RecycleViewAction 這個類來進行操做,它提供了一些經常使用的功能:
  • scrollTo() - Scrolls to the matched View(滑動到匹配試圖);
  • scrollToHolder() - Scrolls to the matched View Holder(滑動到匹配的 ViewHolder);
  • scrollToPosition() - Scrolls to a specific position(滑動到指定位置);
  • actionOnHolderItem() - Performs a View Action on a matched View Holder(在匹配的 ViewHolder 上執行操做);
  • actionOnItem() - Performs a View Action on a matched View(在指定 View 上執行操做);
  • actionOnItemAtPosition() - Performs a ViewAction on a view at a specific position(在指定位置上的 View 執行操做);
// 官方 Demo,這個意思就是直接滑動到中間 Holder 位置,isInTheMiddle() 是自定義匹配器,而後判斷是否滑動成功,對應控件是否已經顯示在頁面上了
// First, scroll to the view holder using the isInTheMiddle matcher.
onView(ViewMatchers.withId(R.id.recyclerView))
.perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()));

// Check that the item has the special text.
String middleElementText =
getApplicationContext().getResources().getString(R.string.middle);
onView(withText(middleElementText)).check(matches(isDisplayed()));

// 自定義的匹配器
private static Matcher<CustomAdapter.ViewHolder> isInTheMiddle() {
return new TypeSafeMatcher<CustomAdapter.ViewHolder>() {
@Override
protected boolean matchesSafely(CustomAdapter.ViewHolder customHolder) {
// 調用的是 holder 中的方法來判斷
return customHolder.getIsInTheMiddle();
}

@Override
public void describeTo(Description description) {
description.appendText("item in the middle");
}
};
}

// 點擊第 40 個 item,而後判斷是否已經顯示在頁面上
// First scroll to the position that needs to be matched and click on it.
onView(ViewMatchers.withId(R.id.recyclerView))
.perform(RecyclerViewActions.actionOnItemAtPosition(40, click()));

// Match the text in an item below the fold and check that it's displayed.
String itemElementText = getApplicationContext().getResources().getString(
R.string.item_element_text) + String.valueOf(40);
onView(withText(itemElementText)).check(matches(isDisplayed()));
官方的第二個 Demo 確定也是測試成功的,由於 RecyclerViewActions.actionOnItemAtPosition 這個方法會先滑動到對應位置
10.Espresso-Intents,是 Espresso 的擴展,它能夠用來驗證和存儲被測試的程序發出的 Intent,不過使用前須要先添加對應依賴:
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.1.0'

// 須要指定 Intent 發出頁面
// 若是要測試意圖這裏須要使用 IntentsTestRule 而不是 ActivityTestRule,不過 IntentsTestRule 是它的之類,作了一些額外的工做
// 例如初始化 Intents,裏面有個變量來手機 Intent
// // Should be accessed only from main thread
// private static final List<VerifiableIntent> recordedIntents = new ArrayList<VerifiableIntent>();
@Rule
public IntentsTestRule<DialerActivity> mActivityRule = new IntentsTestRule<>(
DialerActivity.class);
// 對應權限給足
@Rule public GrantPermissionRule grantPermissionRule = GrantPermissionRule.grant("android.permission.CALL_PHONE");

// 判斷對應 Intent.ACTION_CALL 的 Intent 是否發送
@Test
public void typeNumber_ValidInput_InitiatesCall() {
// Types a phone number into the dialer edit text field and presses the call button.
onView(withId(R.id.edit_text_caller_number))
.perform(typeText(VALID_PHONE_NUMBER), closeSoftKeyboard());
onView(withId(R.id.button_call_number)).perform(click());

// Verify that an intent to the dialer was sent with the correct action, phone
// number and package. Think of Intents intended API as the equivalent to Mockito's verify.
// intended 用於校驗給定匹配器中被測應用是否發送該意圖
intended(allOf(
hasAction(Intent.ACTION_CALL),
hasData(INTENT_DATA_PHONE_NUMBER)));
}

// R.id.button_call_number 觸發的方法
private Intent createCallIntentFromNumber() {
final Intent intentToCall = new Intent(Intent.ACTION_CALL);
String number = mCallerNumber.getText().toString();
intentToCall.setData(Uri.parse("tel:" + number));
return intentToCall;
}

// 若是是 startActivityForResult 的啓動方式,能夠經過這種方式來構建代碼並測試,不過這種只是模擬啓動和數據返回,不會真的啓動頁面,而後模擬調用 // onActivityResult
@Test
public void testStartActivityForResult() {
// 構造結果Intent
Intent resultIntent = new Intent();
resultIntent.putExtra("result", "OK");
Instrumentation.ActivityResult activityResult =
new Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent);

// 若是有相應的 intent 發送,並返回虛構的結果
intending(allOf(
hasComponent(hasShortClassName(".OtherActivity")),
toPackage(TEST_PAGE_NAME)))
.respondWith(activityResult);

// 點擊獲取結果按鈕,這個按鈕就是對應調用 startActivityForResult 方法的按鈕
onView(withId(R.id.for_result_button)).perform(click());

// 查看是否顯示結果
onView(withId(R.id.result_text_view))
.check(matches(withText("OK")));
}

// 驗證 Intent 中參數是否與測試內容一致
intended(allOf(
hasAction(equalTo(Intent.ACTION_VIEW)),
hasCategories(hasItem(equalTo(Intent.CATEGORY_BROWSABLE))),
hasData(hasHost(equalTo("www.google.com"))),
hasExtras(allOf(
hasEntry(equalTo("key1"), equalTo("value1")),
hasEntry(equalTo("key2"), equalTo("value2")))),
toPackage("com.android.browser")));
因此這麼看下來主要就是兩個功能,一個 Intent 的驗證(使用 intended,驗證還能夠驗證 Intent 中的參數是否正確,驗證沒必要以與發送意圖相同的順序發生。從調用Intents.init的時間開始記錄意圖),一個 Intent 的存根(使用 intending,方法說明:啓用存根意圖響應。此方法相似於Mockito.when,當啓動 intent 的活動須要返回數據時(特別是在目標活動是外部的狀況下),此方法特別有用。在這種狀況下,測試做者能夠調用 intending(matcher).thenRespond(myResponse)並驗證啓動活動是否正確處理結果。注意:目標活動不會啓動)。
11.使用 Espresso-Web 測試混合應用程序,也就是 native + WebView,若是隻是想要單獨測試 WebView 可使用 WebDriver 框架來編寫常規的的 Web 測試。
WebDriver 框架使用 Atoms 方式查找與操做 Web 元素,Atoms 相似 ViewAction。
經常使用 API 介紹:
  • onWebView 是 WebView 的入口點;
  • withElement 查找網頁中的元素;
  • check 進行斷言;
  • perform 執行操做;
  • reset 將 WebView 恢復到初始狀態;
// 先經過 Id 找到元素,而後出發點擊事件,並檢查跳轉後的頁面是否包含 navigation_2.html
onWebView()
.withElement(findElement(Locator.ID, "link_2")) // similar to onView(withId(...))
.perform(webClick()) // Similar to perform(click())
// Similar to check(matches(...))
.check(webMatches(getCurrentUrl(), containsString("navigation_2.html")));

public static final String MACCHIATO = "Macchiato";

// 先經過 id 查找 text_input,而後清空以前的輸入,而後輸入 Macchiato,再查找 submitBtn,點擊按鈕,跳到轉新的網頁上, 並檢查這個網頁上是否之
// 前輸入的文字
@Test
public void typeTextInInput_clickButton_SubmitsForm() {
// Create an intent that displays a web form.
Intent webFormIntent = new Intent();
// ...
// Lazily launch the Activity with a custom start Intent per test.
// 每次都要寫
ActivityScenario.launchActivity(webFormIntent);

// Selects the WebView in your layout. If you have multiple WebView objects,
// you can also use a matcher to select a given WebView,
// onWebView(withId(R.id.web_view)).
onWebView()
// Find the input element by ID.
.withElement(findElement(Locator.ID, "text_input"))

// Clear previous input and enter new text into the input element.
.perform(clearElement())
.perform(DriverAtoms.webKeys(MACCHIATO))

// Find the "Submit" button and simulate a click using JavaScript.
.withElement(findElement(Locator.ID, "submitBtn"))
.perform(webClick())

// Find the response element by ID, and verify that it contains the
// entered text.
.withElement(findElement(Locator.ID, "response"))
.check(webMatches(getText(), containsString(MACCHIATO)));
}
在使用 Espresso-Web 是,必定要確保 JS 打開,
@Rule
public ActivityTestRule<WebViewActivity> mActivityRule = new ActivityTestRule<WebViewActivity>(
WebViewActivity.class, false, false) {
@Override
protected void afterActivityLaunched() {
// Technically we do not need to do this - WebViewActivity has javascript turned on.
// Other WebViews in your app may have javascript turned off, however since the only way
// to automate WebViews is through javascript, it must be enabled.
onWebView().forceJavascriptEnabled();
}
};
12.Espresso 提供的一套同步的功能,因此它並不知道任何異步操做,好比後臺線程上運行的操做。若是爲了讓 Espresso 長期運行,那麼就須要爲空閒資源註冊,若是不使用空閒資源這種方式,可使用例如 Thread.sleep 或 CountDownLatch 的方式。
在執行如下操做時,應該考慮使用空閒資源:
  • 從網絡或本地數據源加載數據;
  • 數據庫操做;
  • 使用 Service;
  • 執行復雜的耗時邏輯;
這些操做完成後可能會涉及到 UI 更新,這時就該註冊空閒資源。
可直接使用的空閒資源實現:
  • CountingIdlingResource,計數器,計數器爲零時,關聯的資源被視爲空閒;
  • UriIdlingResource,與上面的類型,不過在資源被視爲空閒以前,計數器須要在特定時間段內爲 0。額外的等待時間會考慮連續的網絡請求;
  • IdlingThreadPoolExecutor,內部實現了線程池,可跟蹤建立的線程池中正在運行的任務總數;
  • IdlingScheduledThreadPoolExecutor,內部實現了 ScheduledThreadPoolExecutor,與上面的同樣,不過它還能夠跟蹤定時執行的任務;
建立本身的 IdlingResource,
public void isIdle() {
// DON'T call callback.onTransitionToIdle() here!
}


public void backgroundWorkDone() {
// 在後臺工做完成後通知
// Background work finished.
callback.onTransitionToIdle() // Good. Tells Espresso that the app is idle.

// Don't do any post-processing work beyond this point. Espresso now
// considers your app to be idle and moves on to the next test action.
}
通常咱們都在 @Before 中註冊空閒資源,而後別忘了在完成了反註冊,代碼以下:

不過這些須要修改代碼,須要在代碼中配合,至少配合通知到空閒資源後臺工做已經完成,而後調用通知方法。
若是不但願過多影響代碼能夠有如下方式,如構建 Gradle's product flavors,僅在調試版本中使用,或使用像 Dagger 的依賴注入框架;

相關文章
相關標籤/搜索