UiAutomator UiAutomator2 检测 Toast 信息方法

非洲赵子龙 for 安卓Espresso菜刀队 · March 17, 2018 · Last by 洛凉 replied at May 10, 2018 · 4421 hits
本帖已被设为精华帖!

小弟不才,也算见识过图片识别的方法检测 toast 信息的,但总觉得它不可靠~自动化要真的落地,用到实处,最重要的就是可靠性! 到底真实为项目带来多大的效率提升,才是真的!但,有一个事实:高层封装的东西越少就越好,因为封装和集成的东西越多,可靠性就会越差!
所以,我们测试人员最好要学会开发的知识,或者说,了解一些开发用到的东西,当然,越多越好!
啰嗦了有点多,还是说主题吧:作为测试人员不要以为一个显示几秒然后就消失的一定是 toast,也有可能是开发自己封装的一个 textview,一定要根据实际项目做甄别,然后写测试代码
以下用实际遇到的问题示例。
应该大多数人都知道移动端的提示信息基本上是用 toast 来实现的,一个可能的实现如下:

public static void showShort(final String info) {
        if (StringUtils.isNotEmpty(info)) {
            BaseApplication.getInstance().getHandler().post(new Runnable() {
                @Override
                public void run() {
                    if (null == mToast) {
                        mToast = Toast.makeText(BaseApplication.getContext(), info, Toast.LENGTH_SHORT);
                    }
                    mToast.setDuration(Toast.LENGTH_SHORT);
                    mToast.setText(info);
                    mToast.show();
                }
            });
        }
    }

是的,这就是对一个 toast 信息如何显示的简单封装。
如果你们开发是差不多以这个方式来实现的 (可以跟开发问,比如哪个功能块,什么业务逻辑会显示某个提示信息,然后是不是大概用Toast.makeText然后show的方法来实现的),那么就可以用下面的方法在自动化代码中进行校验:

  1. 封装一个 Listener 来监听 toast 发出来的事件:
private static void initToastListener() {
        mInstrumentation = InstrumentationRegistry.getInstrumentation();
        mInstrumentation.getUiAutomation().setOnAccessibilityEventListener(new UiAutomation.OnAccessibilityEventListener() {
            @Override
            public void onAccessibilityEvent(AccessibilityEvent event) {
                Log.i("AAA", "onAccessibilityEvent: " + event.toString());
                //判断是否是通知事件
                if (event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
                    return;
                }
                //获取消息来源
                String sourcePackageName = (String) event.getPackageName();
                //获取事件具体信息
                Parcelable parcelable = event.getParcelableData();
                //如果不是下拉通知栏消息,则为其它通知信息,包括Toast
                if (!(parcelable instanceof Notification)) {
                    Log.i("AAA",event.getText().toString());
                    toastMessage = (String) event.getText().get(0);
                    toastOccurTime = event.getEventTime();
                    Log.i("AAA", "Latest Toast Message: " + toastMessage + " [Time: " +         toastOccurTime + ", Source: " + sourcePackageName + "]");
                }else {
                    Log.i("AAA",event.getParcelableData().toString());
                }
            }
        });
    }

原理:通知栏消息和 toast 消息都是AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED的一种,而 Uiautomation 刚好可以设置此事件的监听器,然后我们只需要再继续过滤掉 notification 事件,那剩下的就必然是 toast 信息了!
因此,如果你的 APP 没有启动页 (或者叫闪页),那么直接在 BeforeClass 里初始化这个Listener即可,如果有,可以在Before里 (也就是对每个测试用例注册这个Listener)。为什么呢?因为通常的 APP 的启动页是用来加载一些广告或者其他的信息,也就是说,真正的主页面通常的做法是在启动页 (一般称之为 SplashActivity,可以简单地认为一个 activity 对应一个独立的页面) 之后才加载的;且,我们要测的功能大部分都在 MainActivity 里 (也有可能是其他的 Activity,根据具体情况具体选择)。总之,你要测哪个 activity 下面的 toast 信息,就在哪个 activity 的测试集里初始化,否则,监听不到你想要的信息。示例代码如下:

@BeforeClass
public static void beforeClass(){
        //如果在這里就初始化了,那么监听的会是SplashActivity的信息

    }

@Before
public void startMainActivityFromHomeScreen() {
        initToastListener();
        // Initialize UiDevice instance
        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        // Start from the home screen
        mDevice.pressHome();

        // Wait for launcher
        final String launcherPackage = getLauncherPackageName();
        assertThat(launcherPackage, notNullValue());
        mDevice.wait(Until.hasObject(By.pkg(launcherPackage).depth(0)), LAUNCH_TIMEOUT);

        // Launch the blueprint app
        Context context = InstrumentationRegistry.getContext();
        final Intent intent = context.getPackageManager()
                .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);    // Clear out any previous instances
        context.startActivity(intent);

        // Wait for the app to appear
        mDevice.wait(Until.hasObject(By.pkg(BASIC_SAMPLE_PACKAGE).depth(0)), LAUNCH_TIMEOUT);
    }

下面的是测试用例,检验简单的登录功能 (故意输入错误的密码),看 Toast 信息是否有获取到:

@Test
public void testLoginFailed() {
        mDevice.wait(Until.findObject(By.text("未登录")), LAUNCH_TIMEOUT);
        // Type text and then press the button.
        mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "tv_main_username"))
                .click();
        mDevice.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "et_username")), LAUNCH_TIMEOUT);
        UiObject2 username = mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "et_username"));
        username.clear();
        username.setText(STRING_TO_BE_TYPED);
        UiObject2 password = mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "et_password"));
        password.setText("1234567");
        UiObject2 loginButton = mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "bt_user_login"));
        loginButton.click();
        // Verify the test is displayed in the Ui
        final long startTimeMillis = SystemClock.uptimeMillis();
        boolean isSuccessfulCatchToast;
        while (true) {
            long currentTimeMillis = SystemClock.uptimeMillis();
            long elapsedTimeMillis = currentTimeMillis - startTimeMillis;
            if (elapsedTimeMillis > 5000L) {
                Log.i("AAA", "超过5s未能捕获到预期Toast!");
                isSuccessfulCatchToast = false;
                break;
            }
            if (toastOccurTime > startTimeMillis) {
                isSuccessfulCatchToast = "密码不正确".equals(toastMessage);
                break;
            }
        }
Assert.assertTrue("捕获预期Toast失败!", isSuccessfulCatchToast);

我们看看 Logcat 打印出了什么:
输出结果
Good,我们 catch 到了 Toast 信息。

还有一种是将 textview 作为 toast 的替代方式,这个时候就简单了,因为通常的做法是把它隐藏起来 (暂时不可见,但是 uiautomator2 可以发现它),然后在必要的时候显示出来而已,可能的开发实现代码如下:
设计图
它的 XML 布局:

<TextView
        android:id="@+id/tv_toast_login"
        android:layout_width="@dimen/x264"
        android:layout_height="@dimen/x56"
        android:layout_alignBottom="@id/ll_cotnent"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="@dimen/x10"
        android:background="@drawable/bg_toast_checkcode"
        android:gravity="center"
        android:text="验证码已发送到手机"
        android:textColor="@color/white"
        android:textSize="@dimen/x20"
        android:visibility="visible"/>

注意上面的最后一行 android:visibility="visible"表示默认为可见,如果是写成 android:visibility="gone"就表示默认不可见。要实现 toast 的效果,只要默认设置成 gone,然后在需要的时候 (比如输入的错误的密码然后点击了登录按钮这种情况下) 用一个 handler(这个就讲得太深了,不做细讲) 将它延时展示 (设置成 visible) x 秒再设置成 (invisible 或者 gone) 就可以了。
注意:这里看似只有一个 “验证码已发送到手机”,其实只是一个默认的文本展示值罢了,后续所有的'所谓'toast 信息都是以这个控件来展示的,请知悉。
相应的我们的测试代码如下:

@Test
public void testLoginFailed() {
        mDevice.wait(Until.findObject(By.text("未登录")), LAUNCH_TIMEOUT);
        // Type text and then press the button.
        mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "tv_main_username"))
                .click();
        mDevice.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "et_username")), LAUNCH_TIMEOUT);
        UiObject2 username = mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "et_username"));
        username.clear();
        username.setText(STRING_TO_BE_TYPED);
        UiObject2 password = mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "et_password"));
        password.setText("1234567");
        UiObject2 loginButton = mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "bt_user_login"));
        loginButton.click();
        // Verify the test is displayed in the Ui
        final long startTimeMillis = SystemClock.uptimeMillis();
        boolean isSuccessfulCatchToast;
        //这里其实应该加个超时限制,循环检查,支持超时抛出失败,时间紧迫,就不补充了~
        while (true) {
            UiObject2 uiObject2 = mDevice.wait(Until.findObject(By.res(BASIC_SAMPLE_PACKAGE, "tv_toast_login")), LAUNCH_TIMEOUT);
            Log.i("AAA",uiObject2.toString()+uiObject2.getText()+"_1");
            UiObject2 toastView = mDevice.findObject(By.res(BASIC_SAMPLE_PACKAGE, "tv_toast_login"));
            Log.i("AAA",toastView.toString()+uiObject2.getText()+"_2");
            }
        }

同样的,我们可以看到效果图 (这里就不截图了,有兴趣的可以去试一下) 里已经打印出了这个 tv_toast_login(就是显示 ‘所谓的’toast 信息的载体) 的 text 属性为 “密码不正确”,因此,测试成功!

所以,没有绝对完美且唯一的办法自动化测试一个 APP,最好是要根据实际情况了解实现,对症下药,这样,才能实现自动化的价值!最重要的是,它是可重复利用的,是可靠的!
参考文档:
浅谈 UiAutomator2.0 之 Toast 那些事
Appium 源码之 Toast 检测

共收到 9 条回复 时间 点赞

感谢分享,一直搞不明白 uiautomator 要怎么处理 toast。

思寒_seveniruby 将本帖设为了精华贴 18 Mar 23:57

感谢分享!

学习了
看样子要学习的内容还很多啊

楼主,有办法监听手机中的报错和闪退吗

george 回复

处理崩溃或者错误的办法现在很常见的一种方式是开发多打点 log,然后记录成文件放在一个专门的地方,具体可以参考这个:Android 如何捕获应用的 crash 信息,一般测试人员的话,无法完全测试出所有的报错和闪退的,只能尽量。常用的 BUG 复现工具 (非人工) 倒是可以推荐下这个:replaykit,能重现你的操作 (要先保证网络), 另外,还有一个监听页面前端卡顿的工具可以推荐:AndroidPerformanceMonitor, 其他的也没什么好的推荐了,有更好的,可以互相交流下.

你好,
我想在 uiautomator 中监控到手机中其它模块的报错。你说的方法我试过,好像只能监控自己应用的报错。

george 回复

系统级的报错我还没玩过~尴尬脸...可以谷歌一下.

我们用的 MTK 的手机,只好打开 mtklog,分析 mtklog,这样的局限性比较大。

期待 Python 版本,虽然网上很多,嘿嘿

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 13 Dec 14:44
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up