转载

自动化测试之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 需要通过握手来建立连接,图片是跨进程通信: 自动化测试之Espresso学习

7.测试代码可访问性,通过使用 AccessibilityChecks 类来使用。

8.AndroidJUnitRunner 类是一个JUnit测试运行器,允许您在 Android 设备上运行 JUnit 3 或 JUnit 4 样式的测试类,包括那些使用 Espresso 和 UI Automator 测试框架的测试类。测试运行器处理将测试包和被测应用程序加载到设备,运行测试和报告测试结果。此类替换了 InstrumentationTestRunner 类,该类仅支持 JUnit 3 测试。

自动化测试之Espresso学习

页面依赖或者数据依赖怎么解决?

能否记录流程,比如截图

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 这样的控件):

自动化测试之Espresso学习

如果我们使用的是 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 这个方法会先滑动到对应位置 自动化测试之Espresso学习

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 中注册空闲资源,然后别忘了在完成了反注册,代码如下:

自动化测试之Espresso学习

不过这些需要修改代码,需要在代码中配合,至少配合通知到空闲资源后台工作已经完成,然后调用通知方法。

如果不希望过多影响代码可以有以下方式,如构建 Gradle's product flavors,仅在调试版本中使用,或使用像 Dagger 的依赖注入框架;

自动化测试之Espresso学习

原文  https://juejin.im/post/5d04b06c6fb9a07eb15d544f
正文到此结束
Loading...