前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android UI 测试 - Espresso

Android UI 测试 - Espresso

作者头像
三流编程
发布2018-09-11 16:13:11
1.1K0
发布2018-09-11 16:13:11
举报

Android UI 测试框架,在真机运行,相比手动测试,相当于把流程自动化了,并且自动监测结果。

这篇文章主要是阅读官方文档的结果,这渣英文,不敢说翻译。若有理解错误,望指正。

有些感觉用不着的就舍弃了没有看,当然整篇通读下来,感觉真的开发过程也不会去写这个测试吧,好像学了点用不着的屠龙术。不比单元测试,依然要编译运行到真机上,没敢用公司项目测,只是建了个最简单的 Demo,就感觉好慢,测试一次好慢。要是真的去写这测试,还得写许多代码,考虑许多过程,然后再编译,我怎么觉得,还不如 Instant Run 加自己手动操作测试来得快呢。

当然 Android 工程创建完就自动引入了这个框架,说明肯定是有作用的,大概是自己程度不够,没察觉它能提高多少效率。

设置

测试环境准备

开发者选项中关掉动画:

  • Window animation scale 窗口动画缩放
  • Transition animation scale 过渡动画缩放
  • Animator duration scale 动画程序时长缩放

Gradle 配置

Module 的 gradle 文件中配置

代码语言:javascript
复制
android {
    ...
    defaultConfig {
        ...
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

基本使用

src/androidTest 创建文件。

代码语言:javascript
复制
@RunWith(AndroidJUnit4.class)
@LargeTest
public class HelloWorldEspressoTest {

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

    @Test
    public void listGoesOverTheFold() {
        onView(withText("Hello world!")).check(matches(isDisplayed()));
    }
}
代码语言:javascript
复制
onView(withId(R.id.my_view))        // withId(R.id.my_view) is a ViewMatcher
    .perform(click())               // click() is a ViewAction
    .check(matches(isDisplayed())); // matches(isDisplayed()) is a ViewAssertion
  • ViewMatchers – 当前 View 层级上匹配一个 View
  • ViewActions – 对 View 执行某种行为,如点击
  • ViewAssertions – 检查 View 的状态,类似单元测试中的断言

找到 View

有时候 View 可能没有对应的 R.id,或者虽然有但是不唯一。假设多个 View 共用 R.id.my_viewonView(withId(R.id.my_view)) 会报错,要通过额外内容进行过滤。

代码语言:javascript
复制
onView(allOf(withId(R.id.my_view), withText("Hello!")))
onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

ViewMatchers 提供了若干过滤方法,具体参见 https://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers

  • 页面上任何可与用户交互的 View 都应该有 text 或 content description,如果通过 withText()withContentDescription() 不能获取到 View,说明代码写的不好,补上 text 或 content description。
  • 用最少的过滤方法寻找 View,过滤方法越多,框架做的事情越重,比如能通过 withId 获取到唯一的 View,就不要再 withText 了。
  • 如果 View 在 AdapterView 里,比如 ListView、GridView、Spinner,onView() 方法可能无效,要用 onData() 替换。

View 上执行操作

代码语言:javascript
复制
// 执行点击
onView(...).perform(click());

// 执行多个操作
onView(...).perform(typeText("Hello"), click());

// 如果在 ScrollView 里,要先滚动使 View 在当前页面显示出来,然后再执行其它动作
// 如果 View 本身就在页面中显示,srollTo 不起作用
onView(...).perform(scrollTo(), click());

可执行的操作参见 https://developer.android.com/reference/android/support/test/espresso/action/ViewActions

检查状态

主要通过 .check(matches()) 方法,matches 里是寻找 View 的那些过滤方法,

代码语言:javascript
复制
// 断言 View 没有显示
onView(withId(R.id.bottom_left)).check(matches(not(isDisplayed())));

// 断言 View 不存在了,比如去了另一个 Activity
onView(withId(R.id.bottom_left)).check(doesNotExist());

Adapter View

比如 ListView、GridView、Spinner,有个 Adapter,它有好多个 Item,要寻找内容是 Americano 字符串的 Item

代码语言:javascript
复制
onData(allOf(is(instanceOf(String.class)), is("Americano")));

检查某个数据 Item 没有被加到一个 AdapterView 里,就是说还没有加载到它,Adapter 还没持有这个数据。先自定义一个 Matcher 类

代码语言:javascript
复制
private static Matcher<View> withAdaptedData(final Matcher<Object> dataMatcher) {
    return new TypeSafeMatcher<View>() {

        // 描述自己
        @Override
        public void describeTo(Description description) {
            description.appendText("with class name: ");
            dataMatcher.describeTo(description);
        }

        @Override
        public boolean matchesSafely(View view) {
            if (!(view instanceof AdapterView)) {
                return false;
            }

            @SuppressWarnings("rawtypes")
            Adapter adapter = ((AdapterView) view).getAdapter();
            // 遍历当前 Adapter 持有的数据
            for (int i = 0; i < adapter.getCount(); i++) {
                // 和参数传进来 datamatcher 进行匹配
                if (dataMatcher.matches(adapter.getItem(i))) {
                    return true;
                }
            }

            return false;
        }
    };
}

然后检查

代码语言:javascript
复制
@SuppressWarnings("unchecked")
public void testDataItemNotInAdapter(){
    onView(withId(R.id.list)) // 获取这个列表
        .check(matches(not(withAdaptedData(withItemContent("item: 168")))));
    }
}

list-showing-all-rows.png

假设 ListView,每个 Item 都是一个 Map,如 {"STR" : "item: 0", "LEN": 7},找到内容为 "item: 50" 的并点击

代码语言:javascript
复制
onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))).perform(click());

框架会自动滚动以显示 Item 并点击。

为了寻找 "item: 50" 的 Item,先寻找 AdapterView 用 Map 类型数据填充的,然后再寻找内容,可以定义出一个 Matcher,

代码语言:javascript
复制
return new BoundedMatcher<Object, Map>(Map.class) {
    @Override
    public boolean matchesSafely(Map map) {
        return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
    }

    // 描述自己这个 Matcher
    @Override
    public void describeTo(Description description) {
        description.appendText("with item content: ");
        itemTextMatcher.describeTo(description);
    }
};

定义了 BoundedMatcher 作为 Matcher,便可以使用 withItemContent(equalTo("foo")) 方法,为了方便可以将这个方法再封装

代码语言:javascript
复制
public static Matcher<Object> withItemContent(String expectedText) {
    checkNotNull(expectedText);
    return withItemContent(equalTo(expectedText));
}

然后再使用

代码语言:javascript
复制
onData(withItemContent("item: 50")).perform(click());

找到列表的 Item,还想找到 Item 里面的 View,使用 onChildView(),比如

代码语言:javascript
复制
onData(withItemContent("item: 60")) // 找到 Item
    .onChildView(withId(R.id.item_size)) // 找到 ItemView 里 id 为 R.id.item_size 的 View
    .perform(click());

Recycler View

RecyclerView 的机制和过去的 ListView 这种不同,所以 onData() 方法也不适用了。它需要使用 RecyclerViewActions,有如下可执行的动作:

  • scrollTo() - Scrolls to the matched View.
  • scrollToHolder() - Scrolls to the matched View Holder.
  • scrollToPosition() - Scrolls to a specific position.
  • actionOnHolderItem() - Performs a View Action on a matched View Holder.
  • actionOnItem() - Performs a View Action on a matched View.
  • actionOnItemAtPosition() - Performs a ViewAction on a view at a specific position.

Demo地址

最后来张图

espresso-cheatsheet.png

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.04.27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 设置
    • 测试环境准备
      • Gradle 配置
      • 基本使用
        • 找到 View
          • View 上执行操作
            • 检查状态
            • Adapter View
            • Recycler View
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档