Android UI测试入门

UI测试

UI 测试是为了确保对于用户的UI动作,app能返回正确的UI输出。根据实际实现方案大体可以分为两种:

  • End-To-End(E2E)UI测试,直接通过客户端和后台服务器的交互测试整个系统,普通操作UI,通过网络获取数据,验证UI数据。实现简单,但是存在测试速度缓慢,可能因为网络导致测试用例不通过的问题。
  • 封闭UI测试,测试方法使得测试不需要外部依赖和网络请求,使用Mock Server或者其他方式替代真实的网络请求,只验证UI输出的正确性。

UI测试框架

Android之前比较流行的UI测试框架有robotiumAppiumuiautomatorCalabashEspresso,但是其中Espresso作为Google官方开源的UI测试框架,以其官方的身份、完整的使用文档以及简单的使用方法,快速成为UI测试框架中的主流,本文就是以Espresso框架为主要测试框架。

Espresso

介绍及集成

Espresso 测试框架提供了一组 API 来构建 UI 测试,用于测试应用中的用户流。利用这些 API,您可以编写简洁、运行可靠的自动化 UI 测试。Espresso 非常适合编写白盒自动化测试,其中测试代码将利用所测试应用的实现代码详情。
目前Espresso最新的版本已经出道3.0.1,使用AS创建的工程,默认已经集成了2.2.2版本的Espresso,但是如果要集成最新版本的Espresso库,需要在仓库配置中添加对应仓库地址:

allprojects {
    repositories {
        jcenter()
        maven {
            url "https://maven.google.com"
            //Espresso3.0.1所在仓库地址
        }
    }
}

默认集成的Espresso包espresso-core及其相关依赖包,足以完成一般性的UI测试,除此之外Espresso还有一些扩展包,用于完成一些特殊的测试场景:

  • espresso-web 提供了对WebView测试的相关支持
  • espresso-contrib 提供了对DatePicker, RecyclerView 和 Drawer等控件的特有动作、无障碍以及CountingIdlingResource的支持
  • espresso-intents 用于校验多app测试中intent的正确性
  • espresso-idling-resource(已经包含在core的依赖中)用于处理异步线程同步问题

如果测试过程中不需要上述的扩展功能,则只需要添加core的依赖

dependencies {
    androidTestCompile('com.android.support.test.espresso:espresso-core:3.0.1', {
        exclude group: 'com.android.support', module: 'support-annotations'
        //不导入依赖中的support-annotations,避免出现依赖冲突,会使用用户自己导入的包
    })
}

其余诸如runner,rules包都被core依赖,会自动导入,没有必要手动导入,以免导入版本不正确引起其他问题,除了上面描述的相关库,Espresso还依赖了JUnit和Hamcrest等其他测试辅助框架。

EspressoUI测试的重要对象

  • Espresso Espresso框架的入口类,提供了一些静态方法,便于开始整个测试代码,它提供了类似onView和onData这种方法获取一个可交互的对象ViewInteraction,或者直接进行一个例如页面返回的顶层操作。
  • ViewMatchers 定义了一系列静态方法用于根据不同条件返回Matcher<? super View>对象,作为参数传递给onView()。
  • ViewActions view的操作行为例如click(),最为ViewInteraction.perform()的参数用于对指定View的进行对应操作。
  • ViewAssertions 作为ViewInteraction.check()的参数,判断view的输出是否正确
  • ActivityTestRule 提供了测试单个Activity的功能,当它的launchActivity设置为true时,它会在每个使用@Test注释的方法前和所有注释者@Before的方法前启动。同时可以通过ActivityTestRule对象获取对应Activity的对象。

一个简单的代码示例如下:

@RunWith(AndroidJUnit4.class)
public class LoginTest {
    @Rule
    public ActivityTestRule<LoginActivity> mActivityRule =
            new ActivityTestRule(LoginActivity.class);
            
    @Test
    public void login() throws Exception {
        onView(withId(R.id.et_login_number)).perform(click(),replaceText("17720380994"),closeSoftKeyboard());
        onView(withId(R.id.btn_login_next)).perform(click());
        onView(withId(R.id.et_password)).perform(click(),replaceText("aa123456"),closeSoftKeyboard());
        onView(withId(R.id.btn_login)).perform(click());
        onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
        onView(allOf(instanceOf(ImageButton.class),withParent(withId(R.id.toolbar)),isDisplayed())).perform(click());
        onView(withId(R.id.tv_phone_number)).check(matches(withText("17720380994")));
        onView(IsInstanceOf.<View>instanceOf(ScrollView.class)).perform(swipeUp());
        onView(withId(R.id.tv_exit)).perform(click());
        onView(withText(R.string.exit_login_confirm)).check(matches(isDisplayed()));
        onView(withId(R.id.tv_ok)).perform(click());
        onView(withId(R.id.et_login_number)).check(matches(isDisplayed()));
    }
}

总体来说UI测试的过程就是:找到某个元素,做一些操作,检查结果

寻找View

Espresso中定位View主要有两种,通过页面显示的View特征(onView)和通过数据内容(onData),其中onView用于普通场景,onData用于adapterView这种可能没有渲染的view,但是两者都是基于hamcrest的matcher来进行,本质是相同的不同的是匹配规则

ViewMathcer

ViewMathcer实质上提供了很多Matcher对象,主要用于配合OnView匹配控件,这些Matcher同时可以配合hamcrest中的matcher一起使用,效果更好。常用的Matcher如下

  • withId() onView(withId(R.id.tv_ok))
    直接通过id定位指定的的View,简单粗暴,但是非常实用。
  • isAssignableFrom() onView(isAssignableFrom(ScrollView.class))通过对象类型判断
  • isDisplayed() onView(allOf(isDisplayed(),isAssignableFrom(ScrollView.class))) 通过是否显示判断,通常和其他matcher配合(allOf是hamcrest库重的方法,用于匹配多个matcher,类似的还有anyOf)
  • isEnabled()
  • isFocusable()
    ......

ViewMathcer中几乎把所有的View属性都定义了对应的matcher,需要的可以自行查阅源码或文档。

DataInteraction

DataInteraction 是onData方法的返回值,因为onData方法匹配出的不直接就是View,它匹配的是一个数据集合,只有我们想要进行具体的View操作时,Espresso才会把它转化为View。

 onData(instanceOf(Account.class))

Espresso没有为onData定义Matcher,基本都是使用hamcrest中定义的matcher或者自定义matcher

自定义Matcher

一般自定义Matcher都继承TypeSafeMatcher,需要实现的方法如下

public class CallInfoMatcher extends TypeSafeMatcher<CallInfo> {
    @Override
    public void describeTo(Description description) {
        //匹配失败时的描述,用于描述具体的匹配失败信息
    }

    @Override
    protected boolean matchesSafely(CallInfo item) {
        //具体的匹配过程
        return false;
    }

}

对View的操作

View的操作都是在ViewInteraction上进行的。ViewInteraction也就是onView的返回值对象,用于对于具体的View进行操作(DataInteraction的操作也是转换为ViewInteraction后进行的),ViewInteraction提供了如下方法来对相应的元素做操作:

public ViewInteraction perform(final ViewAction... viewActions) {}

具体的操作通过ViewAction定义,连续操作可以链式调用或者作为参数顺序排列。

ViewAction

ViewAction是espresso中定义的针对View操作的接口类型。ViewAction中实现主要在ViewActions类中通过静态方法提供。常见的action如下

  • click()
  • closeSoftKeyboard()
  • replaceText()
    ......

除去ViewActions提供的较为通用的操作方法,Espresso还提供了很多ViewAction的子类用于完成不同View的特定操作。

ViewAction是在View匹配成功的基础上进行的匹配失败或者匹配不唯一都会导致测试不通过,同时Action与View类型不匹配也会失败

校验结果

测试最重要的一步就是校验结果的正确性,ViewInteraction提供了check()方法用于校验正确性

public ViewInteraction check(final ViewAssertion viewAssert) {
    ......
}

perform()方法类似,check()也是可以链式调用多次校验。

ViewAssertion

ViewAssertion是espresso中定义的用于校验View状态的接口类型,同样ViewAssertion也主要由ViewAssertions中的静态方法提供。其中主要使用的就是matches()方法

public static ViewAssertion matches(final Matcher<? super View> viewMatcher) {
    return new MatchesViewAssertion(checkNotNull(viewMatcher));
}

其中参数viewMatcher就是前面用于匹配View的ViewMatcher

异步问题

Espresso提供了大量的同步机制,这些机制主要针对于主线层的MQ,但是Espresso对于其他的异步操作是无感知的,如果View的显示依赖于网络数据,很有可能就会导致测试用例不通过,因此需要使用前面使用的espresso-idling-resource来保证Espresso在异步线程的可靠性。

espresso-idling-resource依赖添加如下

compile("com.android.support.test.espresso:espresso-idling-resource:3.0.1") {
    exclude module: 'support-annotations'
}
androidTestCompile("com.android.support.test.espresso:espresso-idling-resource:3.0.1") {
    exclude module: 'support-annotations'
}
//由于Espresso对与异步线程无感知,我们需要在代码中主动使用IdlingResource,因此需要使用compile依赖。

IdlingResource

Espresso主要通过IdlingResource这个接口类型完成对异步资源的感知,主要方法如下

public interface IdlingResource {
    //用于标识对于的异步资源
    public String getName();
    //返回目前资源是否可用(闲置),
    public boolean isIdleNow();
    //Espresso会注册此回掉,需要判断资源可用时主动调用
    public void registerIdleTransitionCallback(ResourceCallback callback);
    public interface ResourceCallback {
        public void onTransitionToIdle();
    }
}

Espresso提供了几个IdlingResource的实现类,可以直接使用:

我们借CountingIdlingResource来了解下IdlingResource的主要用法,CountingIdlingResource主要提供的两个共有方法供我们使用

  • increment()计数加一
  • decrement()计数减一,为0时调用onTransitionToIdle()

例如使用网络请求的场景,发起请求时increment()表示资源被占用,请求结束时decrement(),表示资源被释放。同时还需要在测试代码中注册对应资源

 IdlingRegistry.getInstance().register(idlingResource);

IdlingResource解决了异步代码的问题,但是依旧存在问题,我们在业务逻辑代码中创建IdlingResource对象,同时在需要的地方去改变它的状态,然后在测试代码中使用。这无疑是为了测试而给正常的业务代码增加了不必要的逻辑。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,227评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,755评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,899评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,257评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,617评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,757评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,982评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,715评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,454评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,666评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,148评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,512评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,156评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,112评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,896评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,809评论 2 279
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,691评论 2 272

推荐阅读更多精彩内容

  • 是醉是醒 是梦是真 几人能辨 几人能驭 时醉时醒 时梦时真 几时能界 几时能定 似醉非醉 似梦非梦 方为洞明 方能练达
    安素心阅读 215评论 4 6
  • 她说,“有人记得你的生日,就是一种幸福”。我笑着说:“对啊,所以你要惜福。” 今天是她的生日,很久以前就设置了提醒...
    东坡琅阅读 324评论 0 3
  • ActivityLifecycleCallbacks是4.0新增的一个接口,它管理着整个app的所有Activit...
    boboyuwu阅读 865评论 0 1