移动端APP是一个复杂的系统,不同功能之间耦合性很强,很难仅通过单元测试保障整体功能。UI测试是移动应用开发中重要的一环,但是执行速度较慢,有很多重复工作量,为了减少这些工作负担,提高工作效率,需要引入可持续集成的自动化测试方案。
Appium是一款开源测试工具,可以用来测试安卓/iOS/Windows端的原生应用和Web混合应用。
Appium支持多种编程语言,包括Java、Python等,但是直接使用代码维护case可阅读性较差,学习成本也比较高,引入Cucumber可以使用更接近自然语言的方式组织case。 Cucumber是支持BDD(Behaviour-Driven Development,行为驱动开发)的工具,可以自定义语法规则模版,将文本描述的步骤转为使用代码执行的步骤。 由于Cucumber和Java 8均兼容中文文本编码,因此可以自定义中文操作步骤,比起英文代码更易于理解。以定义一个最基本的点击操作为例,预期的语法规则为"当 点击 [元素名称]"
,则可以使用如下定义:
// 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 + "\");";
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
等方式组合定位元素
By by = MobileBy.image(base64ImageString)
。目前不支持多元素查找,只返回第一个查找到的元素。npm install -g opencv4nodejs
StaleElementReferenceException: Appium查找到元素,之后尝试操作元素时,若元素已经不在当前页面DOM资源上时会抛出StaleElementReferenceException
异常。 Appium使用UIAutomator2查找元素时,会保留元素的缓存,对元素进行操作时,会直接把缓存的信息交给UIAutomator2进行点击、滑动等操作。
StaleElementReferenceException
;StaleElementReferenceException
。id:
开头表示通过resource-id查找,text:
开头表示通过文本内容查找),通过HTTP请求返回给客户端(执行单个case时使用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的处理器是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> uriAttributes = queryDecoder.parameters(); //此处仅打印请求参数(你可以根据业务需求自定义处理)
for (Map.Entry> 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的处理器是com.htest.server.handler.ServerHandler,处理消息是按照protobuf格式处理的
@Override protected void handleData(ChannelHandlerContext ctx, MessageModel.Message msg) {
Connection connection = server.getConnectionManager().get(ctx.channel());
connection.updateLastReadTime();
server.getMessageReceiver().onReceive(msg, connection);
}
com.htest.server.handler.ServerChannelHandler
,它也是按照protobuf格式处理消息的,跟HttpServer不同之处在于他们的ChannelInitializer不同3.htest client: Java客户端,用于定义Cucumber步骤,更新手机APK,初始化Appium,执行测试用例; 使用方式:在pc端命令行中执行java -jar htest-client.jar
,pc端需要有Appium和nodejs opencv环境,通过yaml配置文件控制执行测试过程中端参数。 具体工作方式如下:
4.Appium: NodeJS客户/服务端,用于连接手机,通过UIAutomator2/XCUITest,在手机端执行获取元素/点击/滑动等基本操作;
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取