美文网首页
Android Espresso——UI自动化测试框架实践

Android Espresso——UI自动化测试框架实践

作者: HarveyLegend | 来源:发表于2019-07-24 18:00 被阅读0次

    Android Espresso——UI自动化测试框架实践

    Espresso是一个Google官方提供的Android应用UI自动化测试框架。Google希望,当Android的开发者利用Espresso写完测试用例后,能一边看着测试用例自动执行,一边享受一杯香醇Espresso(浓咖啡)。Espresso测试依赖Android设备或模拟器,模拟的是用户的操作,运行测试代码后,应用重新安装在设备上,执行测试代码,执行完即关闭应用。

    Espress有3个特点:

    • 第一个收录在Android Testing Supporting Library底下的测试框架
    • 模拟用户的操作
    • 自动等待,直到UI线程Idle,才会执行测试代码

    1、添加依赖

      androidTestImplementation('com.android.support.test:runner:1.0.0')
      androidTestImplementation 'com.android.support.test:rules:1.0.0'
      androidTestImplementation 'org.hamcrest:hamcrest-library:1.0'
      androidTestImplementation ('com.android.support.test.espresso:espresso-core:3.0.0'){
          exclude(group:'com.google.code.findbugs')
      }
      androidTestImplementation ('com.android.support.test.espresso:espresso-intents:3.0.2')   {
          exclude(group:'com.google.code.findbugs')
      }
      androidTestImplementation ('com.android.support.test.espresso:espresso-contrib:2.2'){
          exclude group: 'com.android.support', module: 'appcompat'
          exclude group: 'com.android.support', module: 'support-v4'
          exclude module: 'recyclerview-v7'
          exclude(group:'com.google.code.findbugs')
      }
    
    

    在android {}的defaultConfig{}中添加

    android {
    
        defaultConfig {
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }
    
    

    2、写用例

    写UI自动化测试用例,归结起来就是3步:

    ~定位View控件 ~操作View控件 ~校验View控件的状态 对应Espresso,就是以下3个方法的调用:

    onView(ViewMatcher)
      .perform(ViewAction)
      .check(ViewAssertion);
    
    

    其中,onView是用来定位View控件的,perform是操作控件的,check是校验View控件的状态。他们各自都需要再传入对应的参数分别如下:

    ViewMatcher,有withId、withText、withClassName等等方法来定位View控件 ViewAction,有click()、longClick()、pressBack()、swipeLeft()等等方法来操作View控件ViewAssertion,有isEnabled()、isLeftOf()、isChecked()等等方法来校验View控件状态 这里有ViewMatcherViewActionViewAssertion的Cheat Sheet。

    public class LoginActivityTest {
    
        //ActivityTestRule 提供了对单个Activity进行测试的方法,可以通过其获取activity中的元素。
        @Rule
        public ActivityTestRule<ActivityRegisterLoginAccountPsw> activityTestRule = new ActivityTestRule<>(ActivityRegisterLoginAccountPsw.class);
    
        //测试方法必须声明为public
        @Test
        public void testLogin() {
            EditText account = activityTestRule.getActivity().findViewById(R.id.et_account);
            if (account.getText().length() == 0) {
                onView(withId(R.id.et_account)).perform(typeText("ordertest102"));
            }
            onView(withId(R.id.et_psw)).perform(typeText("111111"));
            //  onView(withId(R.id.tv_confirm)).perform(click());
    
            //  onView(withText("Masuk")).perform(click());
    
            onView(allOf(withId(R.id.tv_confirm), withText("Masuk"))).perform(click());
    
            IdlingRegistry.getInstance().register(new IdleLoginRequest(3000));
            onView(withId(R.id.et_psw)).check(matches(isDisplayed()));
        }
    }
    
    

    3、IdlingResource

    应用开发中很常见的一个场景是,点击某个按钮,发起网络请求,等请求回来后解析数据,更新界面。或者开始没有加载界面,只有一个ProgressDialog在转圈,这时去定位View是定位不到的,Espresso针对这种测试场景,提供了原生的支持。下面是示例代码,主要是重写isIdleNow()方法,逻辑控制UI线程变Idle的时间。IdlingResource默认的超时时间是26秒,延时时间不要超过这个值,否则espresso会抛出异常。可以通过IdingPolicies.setIdlingResourceTimeout()修改这个超时时间。

    public class IdlingDelayResources implements IdlingResource {
        private boolean timesUp;
        private ResourceCallback mCallback;
    
        public IdlingDelayResources(int delayMillis) {
            new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
                @Override
                public void run() {
                    timesUp = true;
                }
            },delayMillis);
        }
    
        @Override
        public String getName() {
            return IdlingDelayResources.class.getSimpleName();
        }
    
        @Override
        public boolean isIdleNow() {
            if (timesUp && mCallback != null) {
                mCallback.onTransitionToIdle();
            }
            return timesUp;
        }
    
        @Override
        public void registerIdleTransitionCallback(ResourceCallback callback) {
            mCallback = callback;
        }
    }
    
    

    使用方式需要注册和反注册:

    IdlingDelayResources idle = new IdlingDelayResources(1000);

    IdlingRegistry.getInstance().register(idle);
    
    
    IdlingRegistry.getInstance().unregister(idle);
    
    

    注意:实际使用IdlingResource过程中发现在注册IdlingResource后,必须对应一个UI的操作,否则无效。可以理解为必须有一个事件等待主线程变空闲。另外,示例代码只是简单地延时XX秒,并不可靠,需尽量实现在异步任务确定已经完成时,将isIdleNow方法返回true。

    4、自定义Matcher

    如果espresso及hamcrest提供的诸多matcher依然不符合你的需求,可以自定义Matcher进行view的匹配,例如,ListView中item的匹配,Adapter并不是简单的ArrayAdapter时,需要自己定义match规则:

    onData(allOf(is(instanceOf(EntityAdrSlv.Data.class)),withAddress("Kuta"))).inAdapterView(withId(R.id.acty_address_list_lv)).perform(click());
    
    
     public static Matcher<Object> withAddress(final String address) {
            return new BoundedMatcher<Object, EntityAdrSlv.Data>(EntityAdrSlv.Data.class) {
                @Override
                protected boolean matchesSafely(EntityAdrSlv.Data adr) {
                    return address.equals(adr.f7);
                }
    
                @Override
                public void describeTo(Description description) {
                    description.appendText("with address: " + address);
                }
            };
        }
    
    

    在测试类中写一个静态内部类,返回类型为Matcher,内部实现是返回一个TypeSafeMatcher接口或者BoundedMatcher抽象类并实现两个方法。方法matchesSafely(View view)是关键的代码,返回true代表匹配成功,返回false代表匹配失败。describeTo()方法是在匹配失败时的提示信息。

    5、View匹配失败

    如果没有匹配到你想要的View,测试结果会抛出NoMatchingViewException,并将日志输出在控制台,整个View的树结构会被打印出来,类似下面:

    1563853003404.png

    从上面的View结构信息中基本可以发现问题在哪。常见的问题:

    1、匹配到两个或两个以上相同的View,这时可以自定义Matcher,或者用allOf()来多条件匹配。

    2、当前View树并不是你想的那个View,例如开始显示的是一个ProgressDialog加载框,这时你去匹配的树结构是这个ProgressDialog的树结构。

    3、View还没有渲染出来,虽然你写了IdlingResource去延时,但可能在代码逻辑上界面应该显示了,实际上还没有。所以IdlingResource判断UI线程为Idle的时机很重要。

    6、权限怎样跳过

    如果在测试过程中出现权限弹窗,此时使用onView方法匹配一个控件是无法匹配到的,因为此时最顶层的View树结构对应的是Dialog所在的Window。那么如何跳过权限弹窗呢?

    1、官方提供了GrantPermissionRule来为测试代码直接获取权限,使用方式如下:

      @Rule
      public GrantPermissionRule permissionRule = GrantPermissionRule.grant(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION);
    

    注意,@Rule注解标识的变量必须为public类型,grant()方法的参数是一个String...类型的数组,可以传入多个权限类型。

    2、配合UIAutomator来模拟点击弹窗。首先,添加依赖:

    androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
    

    此时运行测试代码,如果你的minSdk<18,会编译失败,因为UiAutomator最小支持的sdk版本是18。但通常我们不会因此就调整项目的minSdk,解决方案是在androidTest下新建一个AndroidManifest.xml

    <manifest
        package="${applicationId}.test"
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <uses-sdk tools:overrideLibrary="android.support.test.uiautomator.v18"/>
    </manifest>
    

    这时就可以运行成功了。那么怎样模拟点击权限弹窗上的允许按钮呢?

    public class PermissionGranter {
    
        private static final int PERMISSIONS_DIALOG_DELAY = 3000;
        private static final int GRANT_BUTTON_INDEX = 1;
    
        public static void allowPermissionsIfNeeded(String permissionNeeded) {
            try {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasNeededPermission(permissionNeeded)) {
                    sleep(PERMISSIONS_DIALOG_DELAY);
                    UiDevice device = UiDevice.getInstance(getInstrumentation());
                    UiObject allowPermissions = device.findObject(new UiSelector()
                            .clickable(true)
                            .checkable(false)
                            .index(GRANT_BUTTON_INDEX));
                    if (allowPermissions.exists()) {
                        allowPermissions.click();
                    }
                }
            } catch (UiObjectNotFoundException e) {
                System.out.println("There is no permissions dialog to interact with");
            }
        }
    
        private static boolean hasNeededPermission(String permissionNeeded) {
            Context context = InstrumentationRegistry.getTargetContext();
            int permissionStatus = ContextCompat.checkSelfPermission(context, permissionNeeded);
            return permissionStatus == PackageManager.PERMISSION_GRANTED;
        }
    
        private static void sleep(long millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                throw new RuntimeException("Cannot execute Thread.sleep()");
            }
        }
    }
    

    切记要在合适的时机调用上面的allowPermissionsIfNeeded方法,比如要在会触发权限申请的页面和操作后再去调用。

    7、经验分享

    1、如何串联整个应用测试,目前已知通过gradle cAT命令执行测试代码默认会执行全部测试方法,但实际运行起来每个方法之间并不是连贯的,每一个方法执行完,页面都会关闭,下一个用例开始时再打开。除非activityRule设置为应用默认启动的activity,通过模拟用户操作到达待测Activity,个人觉得得不偿失,很容易出错,还是尊重google的设计,没必要完全连续的测试整个流程。

    2、可能需要为了测试而去增加业务代码,例如实在无法匹配到view的情况。

    3、测试前建议关闭所有窗口动画,窗口动画、键盘、悬浮通知等都可能导致匹配不到view,导致测试不通过。

    4、Android SDK提供了uiautomatorviewer.bat小工具,来帮你获取当前页面下面的View信息,很好用,在你没有项目代码或者不想去找id的时候,它就派的上用场了。


    1566894357168.png

    5、通常官方提供的matcher不能够完全满足我们的需求,所以自定义matcher很有必要学习一下。目前我们项目中已经提供了多重matcher,以及由matcher衍生出的工具类。

    6、这方面国内的资料比较少,善用StackOverflow和Google。

    8、AppNotIdleException

    当有任务阻塞主线程时,espresso会抛出这个异常。Espresso IdlePolicy默认判断主线程是否空闲的超时时间是60秒,可以通过IdlingPolicies.setMasterPolicyTimeout()方法修改超时时间。用下面的方法打印出当前阻塞UI线程的任务:

     public static void dumpThreads() {
            int activeCount = Thread.activeCount();
            Thread[] threads = new Thread[activeCount];
            Thread.enumerate(threads);
            for (Thread thread : threads) {
                if("RUNNABLE".equals(thread.getState().toString()) && thread.getName().startsWith("AsyncTask")) {
                    LogUtils.e("EspressoTest", thread.getName() + ": " + thread.getState() + "=" + "RUNNABLE".equals(thread.getState().toString()));
                    for (StackTraceElement stackTraceElement : thread.getStackTrace()) {
                        LogUtils.e("EspressoTest", "StackTrace:" + stackTraceElement);
                    }
                }
            }
        }
    

    在我们的项目中是因为依赖了facebook sdk,facebook sdk会在app启动时初始化,其中有两个同步的网络请求阻塞在那里,目前最有效的解决方案就是使用“梯子”,你们懂的~
    9、测试报告
    在Android Studio中打开Terminal命令窗口,输入gradle connectedAndroidTest(简写为gradle cAT)命令,会自动执行所有测试方法,并生成测试报告,存放在app/build/reports中。使用gradle命令测试完会卸载掉应用

    1564368982327.png

    相关文章

      网友评论

          本文标题:Android Espresso——UI自动化测试框架实践

          本文链接:https://www.haomeiwen.com/subject/redolctx.html