移动端APP是一个复杂的系统,不一样功能之间耦合性很强,很难仅经过单元测试保障总体功能。UI测试是移动应用开发中重要的一环,可是执行速度较慢,有不少重复工做量,为了减小这些工做负担,提升工做效率,须要引入可持续集成的自动化测试方案。java
Appium是一款开源测试工具,能够用来测试安卓/iOS/Windows端的原生应用和Web混合应用。node
为了应对快速迭代的移动端应用功能,愈来愈多的App采用混合模式,即将部分功能交给应用内嵌的Web页面实现。Appium能方便的切换测试原生应用或App内嵌的web页面,对于Hybrid App有很好的支持。android
Appium使用各个平台自身提供的测试框架,所以无需引入第三方代码或从新打包应用。web
平台 | 测试框架 |
---|---|
Android 4.2+ | UiAutomator/UiAutomator2(默认) |
Android 2.3+ | Instrumentation(由Selendroid提供) |
iOS 9.3 以上 | XCUITest |
iOS 9.3 如下 | UIAutomation |
Appium在GitHub上开源,维护频率很高,社区也有相对较高的活跃度。在社区的不断努力下,Appium能始终保持兼容最新版本的手机操做系统和官方提供的测试框架,功能也愈来愈完善,包括基本的log收集、录屏、基于opencv的图像识别等,以及最近版本添加的iOS 13/Android 10支持等;面试
Appium支持经过自定义插件寻找元素,GitHub上也有第三方在开发可用插件,例如基于人工智能的icon识别控件示例工程;也能够自定义插件,使用图像识别、OCR等方式查找页面元素。正则表达式
Appium支持多种编程语言,包括Java、Python等,可是直接使用代码维护case可阅读性较差,学习成本也比较高,引入Cucumber可使用更接近天然语言的方式组织case。 Cucumber是支持BDD(Behaviour-Driven Development,行为驱动开发)的工具,能够自定义语法规则模版,将文本描述的步骤转为使用代码执行的步骤。 因为Cucumber和Java 8均兼容中文文本编码,所以能够自定义中文操做步骤,比起英文代码更易于理解。以定义一个最基本的点击操做为例,预期的语法规则为"当 点击 [元素名称]"
,则可使用以下定义:npm
// Cucumber使用正则表达式匹配引号中的内容做为type参数 @当("^点击 \"([^\"]*)\"$") public void findElementAndClick(String type) throws Throwable { // driver为Appium对待测设备的抽象,全部测试步骤最终转为对driver对操做 // type能够传入元素ID对应的字符串,By.id表示经过元素resource-id查找 driver.findElement(By.id(type)).click(); }
编写case时,使用UI自动化测试经常使用的Page Object设计模式,即为APP中须要测试的UI页面定义一个Page对象,该对象中包含页面上的可操做或可校验元素,并添加经常使用方法。编程
以花椒首页为例,能够新建一个名为"首页"的对象,该对象中包含"搜索"、"个人"、"开播"等元素对应的查找方式(例如搜索按钮,对应可用来查找元素的resource-id为com.huajiao:id/main_home_top_search
)。因为在搜索页输入用户uid进行搜索是一个经常使用操做,能够为此定义一个"搜索"方法。 全部测试用例、Page对象、元素、方法都使用测试后台网页进行保存和编辑,而且实现了基本关键词补全功能。设计模式
如上定义基本的点击、滑动、输入文本等操做,创建好适当的页面和方法后,一条用例就能转化为与天然相近的case描述(#
开头行为注释行):缓存
# "$首页.搜索"表示使用"首页"Page中的"搜索"元素 当 点击 $首页.搜索 # "$搜索.搜索()"表示调用搜索页面的搜索方法,括号内为搜索关键词参数 $搜索.搜索(43011080) 当 断言元素出现 $搜索.搜索结果
经过Cucumber定义经常使用操做,如点击、滑动、校验文本等,能够下降编写一条测试用例的工做量,提升测试用例可读性,但并不是全部功能均可以使用经常使用操做的方式。尤为是由于Cucumber只支持一步一步顺序执行指令,没法进行分支或循环指令,所以复杂的操做逻辑须要在自定义步骤中编写代码完成操做。 编写代码部分封装参考Android官方提供的Espresso工程,经过链式调用的方式进行"查找-操做-校验"的流程。
以Android客户端退出登录为例,点击底部"首页-个人"元素,若当前为未登陆状态,则会弹出登录弹出,此时底部"首页-个人"元素不可见,说明已是未登陆状态。
因为Cucumber顺序执行,没法进行"个人"元素可见时退出登录,不可见时关闭登录弹窗
,所以须要编写代码自定义退出登录步骤:
@当("^退出登陆$") public void logout() throws Throwable { // 点击"首页-个人" onView(By.id("com.huajiao:id/bottom_tab_user")).perform(click()); try { // 若是当前用户已登录,不会弹窗提示登录,"首页-个人"元素可见 onView(By.id("com.huajiao:id/bottom_tab_user")).check(matches(isDisplayed())); // 调用退出登陆的方法 logOut(); } // 未登陆状态,"首页-个人"元素不存在,抛出NoSuchElementException catch (NoSuchElementException e) { // 点击系统back键关闭登录弹窗 onActions().pressKeyCode(AndroidKey.BACK, "1").perform(); } }
基本查找方式
By.id
: 经过元素的resource-id进行查找;MobileBy.AndroidUIAutomator(String code)
: 经过UIAutomator2的代码文本查找。code
为符合UIAutomator2规范的代码文本,Appium会解析文本后使用反射的方式调用UIAutomator2进行查找;以下为使用UiSelector
查找文本包含text
的元素: String code = "new UiSelector().textContains(\"" + text + "\");";
xpath查找元素
xpath能够用来在XML文档中查找元素和属性。Appium和谷歌官方提供的uiautomatorviewer工具获取元素都是xml形式组织的,xpath能够精准定位仅靠By.id
、By.className
没法定位的元素:
xpath://*[@text='TEXT')]/../android.widget.TextView[@resource-id='ID']
xpath://*[@resource-id='ID' and @selected='true']/*[@attr='value']
虽然xpath方式查找元素更精准,可是元素的路径可能受到布局改动的影响,且在iOS上性能不佳,所以推荐优先使用
resource-id
等方式组合定位元素
图像识别查找元素
Appium在By Selector级别支持按照图片查找By by = MobileBy.image(base64ImageString)
。目前不支持多元素查找,只返回第一个查找到的元素。
让Appium支持图片查找,须要一点前期准备工做:
安装NodeJS版本的OpenCV库:npm install -g opencv4nodejs
Appium中配置相关参数:
// 设置图片识别阈值,默认0.4。须要尝试在找不到元素和找到不匹配元素间的平衡 driver.setSetting(Setting.IMAGE_MATCH_THRESHOLD, 0.5); // 图片识别耗时较长,能够在操做元素对时候再也不次查找图片,以节省时间 driver.setSetting(Setting.CHECK_IMAGE_ELEMENT_STALENESS, false);
StaleElementReferenceException: Appium查找到元素,以后尝试操做元素时,若元素已经不在当前页面DOM资源上时会抛出StaleElementReferenceException
异常。 Appium使用UIAutomator2查找元素时,会保留元素的缓存,对元素进行操做时,会直接把缓存的信息交给UIAutomator2进行点击、滑动等操做。
StaleElementReferenceException
;StaleElementReferenceException
。id:
开头表示经过resource-id查找,text:
开头表示经过文本内容查找),经过HTTP请求返回给客户端(执行单个case时使用socket方式发送)。执行测试用例过程当中,可能在查找元素时刚好遇到手机端弹窗盖住花椒APP元素等状况,所以在执行测试用例过程当中,会检测手机端可能出现的、非测试步骤中预期的弹窗,包括首充弹窗、开播礼物下载弹窗等,关闭弹窗后再次查找元素;
使用测试平台网页端单次执行测试用例:
若是对软件测试、接口测试、自动化测试、面试经验交流。感兴趣能够加软件测试交流:1085991341,还会有同行一块儿技术交流。
按模块划分,整个框架分为:
测试平台: 网页端,用于保存、编辑基于Cucumber的测试用例,管理Page页面,解析用例中的元素,将转义后的用例发送给客户端,展现客户端实际执行结果;
htest server: Java中间件,使用的netty框架, 负责转发socket消息,即测试平台通知客户端执行用例消息,和客户端执行结果返回测试平台。 使用:
在htest中server端netty的启动com.htest.server.server.BaseServer
@Overridepublic void run() { if (bossGroup == null) { bossGroup = new NioEventLoopGroup(); model.setBossGroup(bossGroup); } if (workerGroup == null) { workerGroup = new NioEventLoopGroup(); model.setWorkGroup(workerGroup); } ServerBootstrap b = new ServerBootstrap(); b.group(model.getBossGroup(),model.getWorkGroup()) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .option(ChannelOption.SO_KEEPALIVE, true) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(getChildHandler()); try { future = b.bind(SERVER_IP, getPort()).sync(); LOGGER.debug("服务启动成功 ip={},port={}",SERVER_IP, getPort()); future.channel().closeFuture().sync(); } catch (Exception e) { LOGGER.error("Exception{}", e); } finally { Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { shutdown(); } }); } }
HttpServer、JarServer、WebsocketServer都是相同的启动方式,区别在于他们监听的端口不一样,处理数据的handler不一样
HttpServer的处理器是com.htest.server.handler.ServerHttpHandler
,处理消息是按照http协议处理的
@Override protected void messageReceived(ChannelHandlerContext ctx, HttpRequest request) { try { this.request = request; headers = request.headers(); if (request.method() == HttpMethod.GET) { QueryStringDecoder queryDecoder = new QueryStringDecoder(request.uri(), Charset.forName("utf-8")); Map<String, List<String>> uriAttributes = queryDecoder.parameters(); //此处仅打印请求参数(你能够根据业务需求自定义处理) for (Map.Entry<String, List<String>> attr : uriAttributes.entrySet()){ for (String attrVal : attr.getValue()) { Logs.HTTP.debug(attr.getKey() + "=" + attrVal); } } } if (request.method() == HttpMethod.POST) { fullRequest = (FullHttpRequest) request; //根据不一样的 Content_Type 处理 body 数据 dealWithContentType(); } keepAlive = HttpHeaderUtil.isKeepAlive(request); writeResponse(ctx.channel(), HttpResponseStatus.OK, "开始执行", keepAlive); } catch (Exception e) { writeResponse(ctx.channel(), HttpResponseStatus.INTERNAL_SERVER_ERROR, "启动失败", true); } }
JarServer的处理器是,处理消息是按照protobuf格式处理的com.htest.server.handler.ServerHandler
@Override protected void handleData(ChannelHandlerContext ctx, MessageModel.Message msg) { Connection connection = server.getConnectionManager().get(ctx.channel()); connection.updateLastReadTime(); server.getMessageReceiver().onReceive(msg, connection); }
WebsocketServer的处理器是com.htest.server.handler.ServerChannelHandler
,它也是按照protobuf格式处理消息的,跟HttpServer不一样之处在于他们的ChannelInitializer不一样
htest client: Java客户端,用于定义Cucumber步骤,更新手机APK,初始化Appium,执行测试用例; 使用方式:在pc端命令行中执行java -jar htest-client.jar
,pc端须要有Appium和nodejs opencv环境,经过yaml配置文件控制执行测试过程当中端参数。 具体工做方式以下:
Appium: NodeJS客户/服务端,用于链接手机,经过UIAutomator2/XCUITest,在手机端执行获取元素/点击/滑动等基本操做;以上内容就是本篇的所有内容以上内容但愿对你有帮助,有被帮助到的朋友欢迎点赞,评论。