专栏首页三流程序员的挣扎Android UI 测试 - Espresso

Android UI 测试 - Espresso

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

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

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

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

设置

测试环境准备

开发者选项中关掉动画:

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

Gradle 配置

Module 的 gradle 文件中配置

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 创建文件。

@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()));
    }
}
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)) 会报错,要通过额外内容进行过滤。

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 上执行操作

// 执行点击
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 的那些过滤方法,

// 断言 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

onData(allOf(is(instanceOf(String.class)), is("Americano")));

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

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;
        }
    };
}

然后检查

@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" 的并点击

onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))).perform(click());

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

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

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")) 方法,为了方便可以将这个方法再封装

public static Matcher<Object> withItemContent(String expectedText) {
    checkNotNull(expectedText);
    return withItemContent(equalTo(expectedText));
}

然后再使用

onData(withItemContent("item: 50")).perform(click());

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Nexus 5X 刷机和 Root

    打开开发者模式,打开“OEM解锁”和“USB调试”,先执行 ./adb reboot bootloader。

    七适散人
  • Android 透明状态栏(伪沉浸式)

    而由于 Android API 的不同,需要考虑 4.4、5.0、6.0 前后的不同。

    七适散人
  • Android 优化——卡顿优化

    Android 系统每隔 16ms 会发出 VSYNC 信号重绘界面(Activity)。之所以是 16ms,是因为 Android 设定的刷新率是 60FPS...

    七适散人
  • 这可能是2020大小厂问的最经典的Android面试题了——事件分发机制、View渲染过程

    Activity和View只有两个方法控制事件传递:dispatchTouchEvent(),onTouchEvent ();

    Android技术干货分享
  • 教你步步为营掌握自定义 View

    国内自定义View的文章汗牛充栋,但是,即使你全部看完它们也未必能掌握这一知识点(实际上,我就几乎看完了所有的国内文章)。为什么?一言以蔽之,你是得其术不明其道...

    非著名程序员
  • View绘制系列(2)-View生命周期

    了解C++的小伙伴们肯定都听过构造函数和析构函数这两个名词,通过构造函数我们可以生成一个类对象,通过析构函数我们可以完成一个对象的销毁,那么对于同样面对对象的J...

    小海编码日记
  • 自定义View基础 - 最易懂的自定义View原理系列(1)

    对于多View的视图,结构是树形结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View,如下图:

    Carson.Ho
  • SAP CDS view里,什么时候用left join,什么时候用association

    版权声明:本文为博主汪子熙原创文章,未经博主允许不得转载。 https://jerry.bl...

    Jerry Wang
  • View详解(1)

    好久好久没更新了,不知道大家还有没有在看以前的一些博文,这段时间换了个坑位还是有点小忙呢!鉴于最近工作接触自定义View,Canvas比较多,所以打算开个系列,...

    小海编码日记
  • Android:你要了解的自定义View基础概念都在这里了!

    自定义View原理是Android开发者必须了解的基础,在了解自定义View之前,你需要有一定的知识储备。

    Android技术干货分享

扫码关注云+社区

领取腾讯云代金券