Skip to content

Latest commit

 

History

History
532 lines (421 loc) · 25.5 KB

README.md

File metadata and controls

532 lines (421 loc) · 25.5 KB

UIGO

📱 零代码快准稳 UI 智能录制回放平台 🚀

3 像素内自动精准定位,2 毫秒内自动精准等待,录制回放快、准、稳!

录制回放 快速上手 在线工具



UIGO - 📱 零代码快准稳 UI 智能录制回放平台 🚀

3 像素内自动精准定位,2 毫秒内自动精准等待,录制回放快、准、稳!
适用于 一次录制到处回放、反复回归界面操作、App UI/功能 自动化测试、
帮助开发快速复现和排查 bug、方便判断 bug 原因出在前端还是后端 等,
大量减少耗时费力又无聊的重复手工操作,大幅提高手工和自动化测试效率,
强力杜绝 测试和开发、前端和后端 关于缺陷单踢皮球等各种低效扯皮内耗!

用户包含腾讯,应微信团队邀请分享了 零代码测试工具与实践(API•单元•UI)

支持功能

  • 零代码 录制和回放 触屏、按键、键盘、数据 等
  • 支持 16:9 标准屏、19.5:9 全面屏等各种设备屏幕
  • 支持原生页面、内置 H5 网页、浏览器加载网页等
  • 支持 Android 真机、Studio/Genymotion 等模拟器
  • 单双指点击、长按、滑动、缩放各种像素级精细操作
  • 自动精准等待、模拟 HTTP API 的请求和响应数据
  • 不同机型录制回放偏差基本仅在 3 像素、2 毫秒 内
  • 可从任意界面开始和停止录制、回放,绕过登录问题
  • 可自动对关键步骤截屏,方便对比回放与录制差异
  • 可自动和手动选择 View 及触摸区域、贴靠方式等
  • 可保存录制步骤相关数据到后端数据库及从后端下载
  • 可用管理端网页浏览检索用例和远程控制录制回放
  • 附带 UnitAuto-机器学习零代码自动化单元测试
  • 中文和英语双语文案,根据系统语言设置自动切换

特点优势

相比各种 UI 录制回放/自动化测试 的 其它平台/工具/框架:

1.它们录制过程各种别扭难用反人类,甚至还需要开发/维护用例脚本、每个用例都写一大堆代码频繁部署等;
UIGO 不需要写任何代码,录制几乎是按和人正常操作完全一样的方式,操作简单易用,录制回放快、准、稳!

2.它们很难兼容各种不同宽高比分辨率屏幕,720P, 1080P 等 16:9 屏幕录制最多只能较好地在 16:9 屏幕回放,
即便手写代码或图像比对等也很难在列表项 View id/图标 重复控件精准定位,经常点错位置导致大量回放失败;
UIGO 则能很好地支持 16:9, 19.5:9 等各种不同屏幕录制,然后在 720P, 1080P, 2K, 1080X2340, 1440X3200
等各种 不同机型、不同系统、不同屏幕 基本都能很好地精准回放,偏差基本仅在相当于一根头发丝的 3 像素内!


3.它们要到处人为设置/调整操作步骤等待时间,还总是要么等太久、要么还没返回就过早执行下一步导致出错,
因为几乎无法保证网络请求在精准时间内返回,所以总是界面没加载完就滑动、弹窗没显示就点了"确定"位置等;
UIGO 则会自动精准等待 App 发送的各种 HTTP API 网络请求,偏差基本在 2 毫秒内,比眨眼一次还要快 50 倍,
像专业的测试工程师一样精准高效地等待数据和 UI 都加载好并执行 点击、长按、滑动、缩放 等每一步对应操作!


原理说明

被测项目不需要写任何用例脚本代码(逻辑代码、注解代码、配置代码等全都不要),
UIGO 会自动录制 UI 触屏操作、虚拟+实体按键操作、HTTP API 网络请求与响应、
Activity, Fragment, Dialog, PopupWindow 等各种组件(控件)元素的生命周期 等,
回放时根据录制触摸点所在被分割球划分的 上、下、左、右、居中、等比 等区域
以及 屏幕分辨率、状态栏高度、导航栏高度、键盘高度 等来自动计算出回放触摸点,
再加上 id(如果有) 相同且距离最近的 View 区域来辅助微调,高度精准回放触屏操作!
对 返回按键、键盘按键 甚至 输入框编辑过程的每个变化的字符 也都能精准无误地还原!


示例项目

UIGO Android 简单测试 App 直接 下载 (第一次可能失败,返回报错 JSON,一般重试一次就可以)
UIGO Android 复杂客户端 App 直接 下载 (第一次可能失败,返回报错 JSON,一般重试一次就可以)

安装 App 必须授权 显示悬浮窗、读写文件存储 这两个权限

其它申请的权限也尽可能都勾选授权,如果不能提前授权,则在使用时弹出是否申请权限弹窗后再确认授权
TommyLemon/APIAuto#61 (comment)


早期零代码单机录制不同分辨率双机同时回放视频

https://www.bilibili.com/video/BV1CK4218788

早期管理端网页工具零代码远程控制手机录制回放视频

https://www.bilibili.com/video/BV1wA4m137ha

早期仿微信朋友圈复杂 App 录制回放,弹窗、输入、网页、滑动、点击等

https://www.bilibili.com/video/BV1fH4y1E7gD

零代码录制回放 H5 移动端网页输入、滑动、点击等操作

https://www.bilibili.com/video/BV1TK421C7y4

录制用例

1.按业务 App 提供的方式打开 UIAuto 管理首页,例如 APIJSONApp 是登录后点击首页标题,UIAuto-Android 是点击首页 [自动 UI 测试] 按钮

2.点击 Record 录制按钮 > 点击顶部悬浮长条中间的 Record 录制 按钮开始录制 > 正常操作 App > 完成一个用例过程后,点击半透明圆形 〇 悬浮球完成录制

3.(可选)点击右下角 post 按钮上传录制的操作和数据等到后端数据库,可先编辑底部的后端服务器 HTTP URL Host 地址为你自己部署的 APIJSON 后端服务

录制注意事项

1.按贴靠方式调整分割球位置及贴靠方式
按下(MotionEvent.ACTION_DOWN 事件)对应 View 的屏幕 [X, Y] 坐标位置前,必须先完成这步(如果已经配置正确则跳过)

分割球对应横纵两条分割线把屏幕分成了 左上、右上、左下、右下 4 个区域,对应数学上按坐标轴划分的 第 二、一、三、四 象限;
点击顶部控制条左上角 # 按钮可切换单双分割球,相比单分割球可更精细地设置高级属性

双分割球中间会出现一个长方形触控区域,默认点击位置按居中处理,可点击切换 center 居中, ratio 等比, top 靠上, bottom 靠下, left 靠左, right 靠右;
分割球本身贴靠方式可以点击分割球来切换 top_left 左上, top_right 右上, bottom_left 左下, bottom_right 右下, ratio 等比,
ratio_top 靠上并垂直纵向等比, ratio_bottom 靠下并垂直纵向等比, ratio_left 靠左并水平横向等比, ratio_right 靠右并水平横向等比

2.滑动要该快则快、该慢则慢
分页列表/网格等滑动尽量快速到底,保证不同宽高比分辨率屏幕上都能在同侧(↓ 从上往下、↑ 从下往上、→ 从左往右、← 从右往左)上显示一致;
点按内部 View 前可缓慢滑动保证目标出现在屏幕内,且手离开屏幕前先在同一位置稳住别动,保证不继续自动滚动(不同机型/系统对惯性滚动处理不一致导致偏差)

3.触控的 View 尽量都有 id,且尽量不要改动
UIGO 会对有 id 的被触控 View 在回放时进行微调触控位置(不改变 UI 布局,仅微调触屏输入坐标位置),灵活适配不同屏幕尤其是不同品牌机型的 UI 差异

4.最好开启托管服务器代理来录制 HTTP API 接口请求与响应流量,用于回放操作时同时回放数据
保证回放到某个步骤和录制到同一步骤时布局一致,不会触控点按错位

5.不要跳出 App,例如跳转外部浏览器或微信、支付宝等其它 App 来上网、登录、分享、支付等
目前不支持跨 App 录制回放,当前 App 外的对应事件录制不到也回放不了,打算后续支持对接支持跨 App 录制回放/自动化测试 的项目,
目前碰到这个问题要么避开,要么等待或手动点 》按钮跳过等待来直接继续执行下一步


回放用例

1.参考 录制用例 1 来打开 UIAuto 管理首页 > 点击左下角 Remote 共享列表 按钮 > 点击打开其中一个和 App 及账号对应的用例,或者 录制用例 后直接进入用例详情界面

2.点击用例详情界面左下角 Replay 回放按钮,返回到用例对应开始录制的业务 App 界面,保证状态一致(都是刚进入后没有操作的界面,或者分页列表都下拉刷新过等)

3.点击顶部悬浮长条中间的 Replay 回放 按钮开始回放,观察每步操作前后,App 的 UI 展示、界面数据、界面跳转、弹窗显示、键盘输入等是否符合预期(和录制时表现一样)

回放注意事项

1.回放开始时 App 初始状态必须和录制时一致
包括所在页面(最好都在首页)、列表/网格滚动距离(最好都是没滚动的初始值 0)、中英文等语言设置、字体大小和样式设置、缓存/调试等自定义配置 等

2.系统的中英文等语言、字体大小和样式 等设置尽可能保持一致

3.最好开启托管服务器代理来回放录制时的 HTTP API 接口请求与响应流量数据


快速上手

可先跳过这个步骤,先下载体验 App 安装包,安装后 按以下 录制用例、回放用例 文档来操作

集成到被测项目 Android 客户端 App

1.依赖 UnitAuto-Apk

UnitAuto-Apk 导入到你项目 app moudule 所在目录settings.gradle

include ':UnitAuto-Apk'

app moudule 目录build.gradle

dependencies {
    api project(':UnitAuto-Apk')
}

2.依赖 UIAuto

UIAuto 导入到你项目 app moudule 所在目录settings.gradle

include ':UIAuto'

app moudule 目录build.gradle

dependencies {
    api project(':UIAuto')
}

2.初始化 UIAuto

Application onCreate 方法 中初始化

    @Override
    public void onCreate() {
        super.onCreate();
        UIAutoApp.STEP_TIMEOUT = 30*1000; // 一般为 HTTP API 网络请求超时时间 ms 毫秒值
        UIAutoApp.getInstance().initUIAuto(this);
    }

3.提供 UIAuto 管理界面入口

AndroidManifest.xml 中注册 UIAutoActivity

<manifest ... >
    <application ... >
      
        <activity
            android:name="uiauto.UIAutoActivity"
            android:label="@string/ui"
            android:windowSoftInputMode="adjustPan"
            android:configChanges="orientation|screenSize"
            android:screenOrientation="portrait"
            />
        <activity
            android:name="uiauto.UIAutoListActivity"
            android:label="@string/ui"
            android:windowSoftInputMode="adjustPan"
            android:screenOrientation="portrait"
            />

        <activity
            android:name="unitauto.apk.UnitAutoActivity"
            android:label="@string/unit"
            android:windowSoftInputMode="adjustPan"
            android:configChanges="orientation|screenSize"
            android:screenOrientation="userLandscape"
            />
      
     </application>
</manifest>

可在你项目的任何界面新增一个按钮或其它形式的入口,仅 DEBUG 模式下展示

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClickUI"
        android:text="UIAutoActivity"
        android:textAllCaps="false"
        />

参考 layout/main_tab_activity

点击这个入口跳转到 UIAutoActivity

    public void onClickUI(View v) {
        startActivity(UIAutoActivity.createIntent(this));
    }

参考 MainTabActivity

4.通知 HTTP API 请求与相应

在 HTTP API 发起请求处加上

		UIAutoApp.getInstance().post(new Runnable() {
			@Override
			public void run() {
				UIAutoApp.getInstance().onHTTPEvent(
					action, type, method, host, url, headerStr, reqBodyStr, null, activity, fragment
				);
			}
		});

参考:
https://github.com/TommyLemon/UIGO/blob/master/APIJSONApp/app/src/main/java/apijson/demo/manager/HttpManager.java#L137-L147

在 HTTP API 响应结果处加上

		UIAutoApp.getInstance().post(new Runnable() {
			@Override
			public void run() {
				UIAutoApp.getInstance().onHTTPEvent(
					- action, responseCode, method, host, url, headerStr, reqBodyStr, resBodyStr, null, activity, fragment
				);
			}
		});

参考:
https://github.com/TommyLemon/UIGO/blob/master/APIJSONApp/app/src/main/java/apijson/demo/manager/HttpManager.java#L168-L179


5.通知 Dialog, PopupWindow 显示和隐藏

业务代码中如果使用了 android.app.AlertDialog,且录制回放用例涉及,则可以换成 uiauto.AlertDialog,
最简单的方式是顶部菜单 Edit > Find > Replace in files 全局搜索 import android.app.AlertDialog,
然后点搜索弹窗右下角 Replace All 按钮批量改为 import uiauto.AlertDialog。
如果因为用了自定义或第三方通用 Base Alert Dialog 不方便替换,则可以 extends uiauto.AlertDialog 或在里面加上:

	@Override
	public void show() {
		super.show();
		UIAutoApp.getInstance().onUIAutoDialogShow(this); // 通知已显示
	}

	// 拦截隐藏事件监听
	private OnDismissListener listener;
	@Override
	public void setOnDismissListener(OnDismissListener listener) {
		this.listener = listener;
	}

	private Activity context;
	private void init(Context ctx) {
		this.context = (Activity) ctx;

		super.setOnDismissListener(new OnDismissListener() {
			@Override
			public void onDismiss(DialogInterface dialog) {
				if (listener != null) {
					listener.onDismiss(dialog);
				}

				UIAutoApp.getInstance().onUIAutoDialogDismiss(Dialog.this); // 通知已隐藏
			}
		});
	}

	// 每个可重写的构造方法都保证调用到 init 方法
	public AlertDialog(Context context) {
		super(context);
		init(context);
	}

	public AlertDialog(Context context, @StyleRes int themeResId) {
		super(context, themeResId);
		init(context);
	}

	public AlertDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
		super(context, cancelable, cancelListener);
		init(context);
	}

所有 Dialog, DatePickerDialog, TimePickerDialog, ProgressDialog, CharacterPickerDialog 等也同上处理。


业务代码中如果使用了 android.widget.PopupWindow,且录制回放用例涉及,则可以换成 uiauto.PopupWindow,
最简单的方式是顶部菜单 Edit > Find > Replace in files 全局搜索 import android.widget.PopupWindow,
然后点搜索弹窗右下角 Replace All 按钮批量改为 import uiauto.PopupWindow。
如果因为用了自定义或第三方通用 Base Popup Window 不方便替换,则可以 extends uiauto.PopupWindow 或在里面加上:

    private android.widget.PopupWindow popupWindow;
    private View view;

    @Override
    public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        super.showAsDropDown(anchor, xoff, yoff, gravity);

        if (view == null) {
            try {
                Field field = android.widget.ListPopupWindow.class.getDeclaredField("mPopup");
                field.setAccessible(true);
                popupWindow = (android.widget.PopupWindow) field.get(this);
                //  popupWindow.setOutsideTouchable(false);

                Field dvField = android.widget.PopupWindow.class.getDeclaredField("mDecorView");
                dvField.setAccessible(true);
                view = (View) dvField.get(popupWindow);

                if (view == null) {
                    View cv = popupWindow.getContentView();
                    view = cv == null ? null : cv.getRootView();
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }

        if (view == null) {
            View cv = getContentView();
            view = cv == null ? null : cv.getRootView();
        }

        Window w = getWindow();

        // 通知已显示
        UIAutoApp app = UIAutoApp.getInstance();
        app.onUIAutoWindowCreate(w.getCallback(), w);
        app.setCurrentPopupWindow(popupWindow, view, null, context, null);
    }


    // 拦截隐藏事件监听
    private android.widget.PopupWindow.OnDismissListener listener;
    @Override
    public void setOnDismissListener(android.widget.PopupWindow.OnDismissListener listener) {
        this.listener = listener;
    }

    public Window getWindow() {
        Activity ctx = view == null ? null : (Activity) view.getContext();
        if (ctx == null) {
            ctx = context;
        }
        return ctx.getWindow();
    }

    private Activity context;
    private void init(Context ctx) {
        this.context = (Activity) ctx;

        super.setOnDismissListener(new android.widget.PopupWindow.OnDismissListener() {
            @Override
            public void onDismiss() {
                if (listener != null) {
                    listener.onDismiss();
                }

                Window w = getWindow();

                // 通知已隐藏
                UIAutoApp app = UIAutoApp.getInstance();
                app.onUIAutoWindowDestroy(w.getCallback(), w);
                app.setCurrentPopupWindow(null, null, null, context, null);
            }
        });
    }

    // 每个可重写的构造方法都保证调用到 init 方法
    public PopupWindow(Context context) {
        super(context);
        init(context);
    }

    public PopupWindow(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public PopupWindow(Context context, AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public PopupWindow(Context context, AttributeSet attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    public PopupWindow(Context context, View contentView) {
        super(contentView);
        init(context);
    }

    public PopupWindow(Context context, int width, int height) {
        super(width, height);
        init(context);
    }

    public PopupWindow(Context context, View contentView, int width, int height) {
        super(contentView, width, height);
        init(context);
    }

    public PopupWindow(Context context, View contentView, int width, int height, boolean focusable) {
        super(contentView, width, height, focusable);
        init(context);
    }

所有 ListPopupWindow 等也同上处理。

Java 后端 Server

可先跳过,使用 http://apijson.cn:8080http://apijson.cn:9090 代替

见 APIJSON-Demo 后端上手
https://github.com/APIJSON/APIJSON-Demo?tab=readme-ov-file#1%E5%90%8E%E7%AB%AF%E4%B8%8A%E6%89%8B


录制、回放用例

见以上 录制用例回放用例 的说明。



常见问题

1.apijson.cn 访问不了

托管服务地址改为 http://47.98.196.224:8080
TommyLemon/APIAuto#13


更多常见问题
https://github.com/TommyLemon/APIAuto/issues


技术交流

关于作者

https://github.com/TommyLemon

如果有什么问题或建议可以 去 APIAuto 提 issue,交流技术,分享经验。
如果你解决了某些 bug,或者新增了一些功能,欢迎 提 PR 贡献代码,感激不尽。

其它项目

APIJSON 🚀 腾讯零代码、全功能、强安全 ORM 库 🏆 后端接口和文档零代码,前端(客户端) 定制返回 JSON 的数据和结构

APIAuto 敏捷开发最强大易用的 HTTP 接口工具,机器学习零代码测试、生成代码与静态检查、生成文档与光标悬浮注释,集 文档、测试、Mock、调试、管理 于一体的一站式体验

UnitAuto 机器学习零代码单元测试平台,零代码、全方位、自动化 测试 方法/函数 的正确性、可用性和性能

SQLAuto 智能零代码自动化测试 SQL 语句执行结果的数据库工具,任意增删改查、任意 SQL 模板变量、一键批量生成参数组合、快速构造大量测试数据

Android-ZBLibrary Android MVP 快速开发框架,Demo 全面,注释详细,使用简单,代码严谨

持续更新

https://github.com/TommyLemon/UIGO/commits/master

我要赞赏

UIGO 从私有仓库默默开发了 3 年多到现在终于开源了,Apache 证书对商用和非商用都很友好。
创作不易、坚持更难,右上角点亮 ⭐ Star 支持/收藏下本项目吧,谢谢 ^_^
https://github.com/TommyLemon/UIGO