📱 零代码快准稳 UI 智能录制回放平台 🚀
3 像素内自动精准定位,2 毫秒内自动精准等待,录制回放快、准、稳!
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,一般重试一次就可以)
其它申请的权限也尽可能都勾选授权,如果不能提前授权,则在使用时弹出是否申请权限弹窗后再确认授权
TommyLemon/APIAuto#61 (comment)
https://www.bilibili.com/video/BV1CK4218788
https://www.bilibili.com/video/BV1wA4m137ha
https://www.bilibili.com/video/BV1fH4y1E7gD
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 安装包,安装后 按以下 录制用例、回放用例 文档来操作
把 UnitAuto-Apk 导入到你项目 app moudule 所在目录,settings.gradle 中
include ':UnitAuto-Apk'
dependencies {
api project(':UnitAuto-Apk')
}
把 UIAuto 导入到你项目 app moudule 所在目录,settings.gradle 中
include ':UIAuto'
dependencies {
api project(':UIAuto')
}
在 Application onCreate 方法 中初始化
@Override
public void onCreate() {
super.onCreate();
UIAutoApp.STEP_TIMEOUT = 30*1000; // 一般为 HTTP API 网络请求超时时间 ms 毫秒值
UIAutoApp.getInstance().initUIAuto(this);
}
在 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));
}
在 HTTP API 发起请求处加上
UIAutoApp.getInstance().post(new Runnable() {
@Override
public void run() {
UIAutoApp.getInstance().onHTTPEvent(
action, type, method, host, url, headerStr, reqBodyStr, null, activity, fragment
);
}
});
在 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
);
}
});
业务代码中如果使用了 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 等也同上处理。
可先跳过,使用 http://apijson.cn:8080 或 http://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
托管服务地址改为 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