自动化测试之Espresso学习

1,403 阅读11分钟
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 的依赖注入框架;