说明
本次分享Android端APP自动化测试Excel执行逻辑,整体逻辑与Web端类似,没看过的读者可看这里 =========>>
一种很轻松的Excel关键字方式进行网页Web自动化测试(Java+Selenium+TestNG+Excel)
当然也存在差异的部分,本文会详细介绍该部分------------------(PS:作者使用版本appium框架 java版本8.0.0)
可能小伙伴会有疑问,整体框架看着可能有点臃肿(cn.nhdc.cloud.testscripts包下为测试脚本部分),代码不够简洁。
- 那是因为:
*设计之初,为提供测试人员一种只专注业务逻辑测试代码,快捷开发测试脚本(testcase)又同时加强代码可读性,case高可维护性的一种方案,同时考虑不同应用设备端、不同类型的测试,提供统一的一套框架。但前景美好,现实很残酷。。。跑题了 ,总而言之,为了能快速落地,形成了目前这套框架,即不是专门为了Excel测试设计的,是为了多种形式调用测试及扩展开发设计的,so... **
总之,就在此基础上,提供Excel执行测试的一种方案,同时结合EasyExcel工具提供用户快速编辑测试用例,整体形成一个闭环且相对便利的App/Web自动化测试解决方案
- 文章作者:@随心自然fqc , 转载时,请注明来源,注明作者,这是对文章作者的尊重,也是对知识的尊重。
整体图解
主要内容
1、测试调用方式
与Web类型调用一致,同时testUseBaseMethod(username,password)调用基类关键字操作方法。
import cn.nhdc.cloud.modules.casemanage.model.vo.TestCaseVo;
import cn.nhdc.cloud.testscripts.config.AppiumCapabilities;
import cn.nhdc.cloud.testscripts.config.LocateType;
import cn.nhdc.cloud.testscripts.listener.ExtentTestNGIReporterListener;
import cn.nhdc.cloud.testscripts.testcase.base.AndroidTestBase;
import org.testng.annotations.Listeners;
import org.testng.annotations.Optional;
import org.testng.annotations.Test;
import java.util.List;
/**
* Android类型 Excel执行方式 自动化测试demo <br>
* Method 1 : {@link #testExcelExcute} 正向逻辑执行 <br>
* Method 2 : {@link #testExcelInvokeExcute} 反射逻辑执行 <br>
* Method 3 : {@link #testUseBaseMethod} 同时可调用基类方法 <br>
*
* ps: 以上2种方式 原理不同 但执行效果类似 具体选择哪种取决于调用者意愿
*
* @author Fan QingChuan
*/
@Test(description = "Excel方式自动化测试示例-正向逻辑&反射执行逻辑")
@Listeners(value = ExtentTestNGIReporterListener.class)
public class ExcelAndroidTestDemo extends AndroidTestBase {
@Test(description = "正向逻辑: 操作方法封装在 AndroidTestBase 中, 测试用例中可直接调用 解析Excel -> caseSteps 根据遍历操作编码switch执行各项操作")
void testExcelExcute(
@Optional("C:\\Users\\allen\\Desktop\\自动化测试_测试用例_2022_07_04_150350_623.xlsx") String fileName,
@Optional("testcase")String sheetName,@Optional("null")Integer headerRowNumber) {
List<TestCaseVo> caseVoList = analysisExcelUiCase(fileName, sheetName, headerRowNumber);
excuteExcelUiTest(caseVoList);
}
@Test(description = "反射逻辑: 操作封装在 AndroidCommon 通过 AndroidTestBase 中反射获取 AndroidBasePage.class 并实例化 再根据操作编码+分类枚举 获取对应方法 并反射(invoke)执行测试")
void testExcelInvokeExcute(
@Optional("C:\\Users\\allen\\Desktop\\自动化测试_安卓APP类型测试用例_2022_07_08_133321_663.xlsx") String fileName,
@Optional("testcase")String sheetName,@Optional("null")Integer headerRowNumber)
throws IllegalAccessException, InstantiationException {
List<TestCaseVo> caseVoList = analysisExcelUiCase(fileName, sheetName, headerRowNumber);
invokeExcelUiTest(caseVoList);
}
@Test
void testUseBaseMethod(@Optional("ATE002")String username,@Optional("a123456")String password) {
//开启Appium服务
startDefaultBaseService();
//初始化AndroidDriver
initBaseAndroidDriver(AppiumCapabilities.getHarmonyJx());
//点击同意隐私协议
clickNegatively(LocateType.ID,"cn.newhope.qc:id/tv_privacy_agree");
//输入用户名
inputText(LocateType.ID,"cn.newhope.qc:id/etAccount",username);
//输入密码
inputText(LocateType.ID,"cn.newhope.qc:id/etPassword",password);
//点击勾选同意隐私政策
click(LocateType.ID,"cn.newhope.qc:id/protocolCb");
//点击登录
click(LocateType.ID,"cn.newhope.qc:id/btnLogin");
//断言toast
assertToastHasAppeared(10,"toast消息文本");
}
}
2、执行测试逻辑
整体执行逻辑与Web一致,只是在App自动化方面,Appium ,执行测试前需开启Appium服务(AppiumDriverLocalService),并实例化具体不同测试端的Driver。
强烈建议:看过Web端相关逻辑再来看Android端 传送门 >> 网页Web端Excel自动化测试(Java+Selenium+TestNG+Excel)
- 先来App通用的测试基类--AppTestBase, 主要提供创建/关闭Appium服务
import cn.hutool.core.util.ObjectUtil;
import cn.nhdc.cloud.modules.casemanage.model.vo.TestCaseVo;
import cn.nhdc.cloud.testscripts.config.ReportLog;
import cn.nhdc.cloud.testscripts.testcase.base.common.AppCommon;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException;
import io.appium.java_client.service.local.flags.ServerArgument;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;
import java.net.URL;
import java.util.List;
import java.util.Map;
/**
* App相关测试通用Base <br>
* 继承该类需实现抽象方法-Excel执行测试方法 {@link AppTestBase#excuteExcelUiTest(List)} {@link AppTestBase#invokeExcelUiTest(List)} <br>
* 该类只提供AppiumDriverLocalService {@link AppTestBase#service} 不提供具体类型baseDriver <br>
* 即:
* 测试case应早已明确该从事具体类型Driver的测试,应选择继承该类子类 {@link AndroidTestBase} 或 {@link IOSTestBase} 或其他
*
* @author Fan QingChuan
* @since 2022/6/7 16:53
*/
public abstract class AppTestBase extends WebTestBase implements AppCommon {
private static final ReportLog reportLog = new ReportLog(AppTestBase.class);
public static final int TIME_OUT = 5;
public static final int SLEEP_TIME = 300;
public static final int DEFAULT_PORT = 4723;
public static final String DEFAULT_IP = "127.0.0.1";
protected AppiumDriverLocalService service;
@Override
public abstract void excuteExcelUiTest(List<TestCaseVo> caseVoList);
@Override
public abstract void invokeExcelUiTest(List<TestCaseVo> caseVoList) throws InstantiationException, IllegalAccessException;
public void startDefaultBaseService() {
try {
service = buildDefaultAppiumService(); //创建默认的AppiumDriverLocalService
checkPortAndKillTask(DEFAULT_PORT); //检查并KILL端口进程任务 默认4723
service.start(); //开启服务
reportLog.info("开启Appium本地服务 ========== >> URL: {}",service.getUrl());
} catch (AppiumServerHasNotBeenStartedLocallyException e) {
e.printStackTrace();
}finally {
reportLog.info("检查服务是否正常运行 ========== >> [{}]",service.isRunning());
}
}
public void startCustomBaseService(int port, String ipAddress, Map<ServerArgument,String> arguments) {
try {
service = buildCustomAppiumService(port,ipAddress,arguments); //根据配置创建AppiumDriverLocalService
checkPortAndKillTask(port);
service.start();
reportLog.info("开启Appium本地服务 ========== >> URL: [{}]",service.getUrl());
} catch (AppiumServerHasNotBeenStartedLocallyException e) {
e.printStackTrace();
}finally {
reportLog.info("服务是否正常开启 ========== >> [{}]",service.isRunning());
}
}
public URL getServiceUrl() {
if (service.isRunning()) {
return service.getUrl();
}else {
throw new RuntimeException("AppiumDriverLocalService service 未启动!");
}
}
protected void closeBaseAppiumService() {
closeAppiumService(service);
}
注意此处创建/关闭的服务是AppTestBase的AppiumDriverLocalService,如果测试中需要创建自己定义的Appium服务,则看后续 AppCommon
-
App层面测试通用--AppCommon
initOrConnectAppiumService(String initInfos) //根据初始化配置JSON信息(initInfos) 初始化创建AppiumServiceEntity * 注意此处不开启服务,由具体测试端去开启服务并实例化Driver
buildDefaultAppiumService() //创建默认的AppiumDriverLocalService
buildCustomAppiumService(int port, String ipAddress, Map<ServerArgument,String> arguments) //创建自定义的AppiumDriverLocalService
closeAppiumService(AppiumDriverLocalService service) //关闭AppiumDriverLocalService
import cn.hutool.core.util.ObjectUtil;
import cn.nhdc.cloud.common.utils.*;
import cn.nhdc.cloud.testscripts.config.AppiumServiceEntity;
import cn.nhdc.cloud.testscripts.config.ReportLog;
import cn.nhdc.cloud.testscripts.testcase.base.AppTestBase;
import cn.nhdc.common.util.CollectionUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServiceBuilder;
import io.appium.java_client.service.local.flags.GeneralServerFlag;
import io.appium.java_client.service.local.flags.ServerArgument;
import java.io.File;
import java.util.Map;
public interface AppCommon extends WebCommon{
int APP_COMMON_TIME_OUT = AppTestBase.TIME_OUT;
int APP_COMMON_SLEEP_TIME = AppTestBase.SLEEP_TIME;
int APP_COMMON_DEFAULT_PORT = AppTestBase.DEFAULT_PORT;
String APP_COMMON_DEFAULT_IP = AppTestBase.DEFAULT_IP;
ReportLog reportLog = new ReportLog(AppCommon.class);
default void checkPortAndKillTask(int port) {
if (CommonUtils.isWindows()) {
if (SocketUtils.isPortBeUsed(port)) {
String rs = CommonUtils.stopWindowsAppiumService(port);
reportLog.info(" ========== >> 端口[{}]被占用 taskkill {} ", port, rs);
}
}else {
if (SocketUtils.isPortBeUsed(port)) {
String rs = CommonUtils.stopLinuxAppiumService(port);
reportLog.info(" ========== >> 端口[{}]被占用 taskkill {} ", port, rs);
}
}
}
default AppiumDriverLocalService buildCustomAppiumService(int port, String ipAddress, Map<ServerArgument,String> arguments) {
AppiumServiceBuilder builder = getAppiumServiceBuilder(port, ipAddress, arguments);
return AppiumDriverLocalService.buildService(builder).withBasePath("/wd/hub/");
}
default AppiumDriverLocalService buildDefaultAppiumService() {
return AppiumDriverLocalService.buildDefaultService().withBasePath("/wd/hub/");
}
default AppiumServiceBuilder getAppiumServiceBuilder(int port, String ipAddress, Map<ServerArgument,String> arguments) {
if (!MathUtils.rangeInDefined(port,4700,5000)) {
throw new RuntimeException("端口限定范围 4700-5000");
}
if (!IpUtils.internalIp(ipAddress)) {
throw new RuntimeException("IP地址格式不合法!");
}
AppiumServiceBuilder builder = new AppiumServiceBuilder();
builder.withIPAddress(ipAddress);
builder.usingPort(port);
//默认设置 session覆盖 日志级别info 日志位置 调用者可覆盖
builder.withArgument(GeneralServerFlag.SESSION_OVERRIDE);
builder.withArgument(GeneralServerFlag.LOG_LEVEL,"info");
builder.withLogFile(new File(CommonUtils.getOutPutRootPath() + "nhdc-cloud-test-platform"
+ CommonUtils.SEPARATOR + "appium-log" + CommonUtils.SEPARATOR + DateUtils.getTimeSuffix() + "_info.log"));
if (CollectionUtils.isNotEmpty(arguments)) {
arguments.forEach((k,v) -> {
if (ObjectUtil.isEmpty(v)) {
builder.withArgument(k);
}else {
String argName = k.getArgument();
switch (argName) {
case "--port":
case "-p":
builder.usingPort(Integer.parseInt(v));
break;
case "--address":
case "-a":
builder.withIPAddress(v);
break;
case "--log":
case "-g":
builder.withLogFile(new File(v));
break;
default:
builder.withArgument(k,v);
break;
}
}
});
}
return builder;
}
default void closeAppiumService(AppiumDriverLocalService service) {
try {
if (service.isRunning()) {
service.stop();
}
} catch (Exception e) {
e.printStackTrace();
}finally {
reportLog.info(" ========== >> Appium服务是否正常关闭 [{}]",!service.isRunning());
}
}
default AppiumServiceEntity initOrConnectAppiumService(String initInfos) {
if (ObjectUtil.isEmpty(initInfos)) {
throw new IllegalArgumentException("initInfos 配置信息不能为空!");
}
if (CommonUtils.isJSONString(initInfos)) {
throw new IllegalArgumentException("initInfos配置信息JSON 格式不合法! 请检查!");
}
JSONObject initJson = JSON.parseObject(initInfos);
if (!initJson.containsKey("isStartDefaultService")) {
throw new IllegalArgumentException("initInfos 中未配置 isStartDefaultService true/false");
}
Boolean isStartDefaultService = initJson.getBoolean("isStartDefaultService");
if (ObjectUtil.isEmpty(isStartDefaultService)) {
throw new IllegalArgumentException("initInfos 中未正确配置 isStartDefaultService true/false");
}
reportLog.info(" ========== >> isStartDefaultService [{}]",isStartDefaultService);
AppiumServiceEntity serviceEntity = new AppiumServiceEntity();
if (isStartDefaultService) {
AppiumServiceBuilder builder = getAppiumServiceBuilder(APP_COMMON_DEFAULT_PORT, APP_COMMON_DEFAULT_IP, null);
AppiumDriverLocalService service = AppiumDriverLocalService.buildService(builder).withBasePath("/wd/hub/");
reportLog.info(" ========== >> 构建AppiumDriverLocalService 等待启动服务 [{}]",service.getUrl());
serviceEntity.setService(service);
}else {
int port = initJson.getIntValue("port");
String ipAddress = initJson.getString("ipAddress");
String basePath = initJson.getString("basePath");
String url = "http://" + ipAddress + ":" + port + basePath;
serviceEntity.setUrl(url);
reportLog.info(" ========== >> 用户指定Appium服务地址 [{}]",url);
}
return serviceEntity;
}
- AppiumServiceEntity类
import io.appium.java_client.service.local.AppiumDriverLocalService;
import lombok.Data;
/**
* @author Fan QingChuan
*/
@Data
public class AppiumServiceEntity {
private String url;
private AppiumDriverLocalService service;
}
自此,无论是使用AppTestBase还是自创的,Appium服务已创建好,下一步开启具体Android测试
- 测试基类--AndroidTestBase
import cn.hutool.core.util.ObjectUtil;
import cn.nhdc.cloud.modules.casemanage.model.vo.TestCaseVo;
import cn.nhdc.cloud.testscripts.config.AppiumServiceEntity;
import cn.nhdc.cloud.testscripts.config.ReportLog;
import cn.nhdc.cloud.testscripts.enums.AndroidActionTypeEnum;
import cn.nhdc.cloud.testscripts.page.base.AndroidBasePage;
import cn.nhdc.cloud.testscripts.testcase.base.common.AndroidCommon;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.touch.offset.PointOption;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.testng.ITestResult;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeSuite;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
/**
* Android相关测试基类 <br>
* = Android相关的操作方法调用方式与 {@link WebTestBase}一致 区别在该基类提供特殊于Android端的操作 示例 <br>
* {@link cn.nhdc.cloud.testscripts.testcase.demo.JxAppLoginTestDemo} <br>
* {@link cn.nhdc.cloud.testscripts.testcase.demo.JxWithLocalServiceTestDemo} <br>
* {@link cn.nhdc.cloud.testscripts.testcase.demo.JxAndroidSummaryTestDemo} <br>
* = 同样提供Excel执行方式 示例 <br>
* {@link cn.nhdc.cloud.testscripts.testcase.demo.ExcelAndroidTestDemo} <br>
* = 同时进行 Web与App测试 示例 <br>
* {@link cn.nhdc.cloud.testscripts.testcase.demo.WebAndAppTestDemo} <br>
* @author Fan QingChuan
* @since 2022/7/3 18:11
*/
public class AndroidTestBase extends AppTestBase implements AndroidCommon {
private static final ReportLog reportLog = new ReportLog(AndroidTestBase.class);
protected AndroidDriver baseAndroidDriver;
@Override
public void excuteExcelUiTest(List<TestCaseVo> caseVoList) {
//此处省略N行代码,作者懒,switch case方式要写贼多代码 丑得YP 作者已经放弃此种方式了
}
@Override
public void invokeExcelUiTest(List<TestCaseVo> caseVoList) throws InstantiationException, IllegalAccessException {
Class<?> clazz = null;
try {
clazz = Class.forName("cn.nhdc.cloud.testscripts.page.base.AndroidBasePage");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
assert clazz != null;
AndroidBasePage instance = (AndroidBasePage) clazz.newInstance();
Class<?> finalClazz = clazz;
AtomicReference<AppiumServiceEntity> serviceEntity = new AtomicReference<>();
caseVoList.forEach(caseVo -> {
List<TestCaseVo.UiCaseStepVo> caseSteps = caseVo.getCaseSteps().stream().sorted(Comparator.comparingInt(TestCaseVo.UiCaseStepVo::getSort)).collect(Collectors.toList());
caseSteps.forEach(step -> {
try {
reportLog.info("测试用例编号[{}] 步骤序号:[{}],测试步骤描述 [{}],操作编码:[{}],元素定位方式[{}],定位值[{}],输入参数值[{}]",step.getCaseCode(),step.getSort(),step.getDescription(),step.getActionKeyword(),step.getElementLocateType(),step.getElementLocateValue(),step.getParameter());
if (step.getActionKeyword().equalsIgnoreCase(AndroidActionTypeEnum.InitOrConnectAppiumService.getActionKeyword())) {
//根据用户配置初始化创建AppiumServiceEntity
Method method = finalClazz.getMethod(step.getActionKeyword(),String.class);
serviceEntity.set((AppiumServiceEntity) method.invoke(instance, step.getParameter()));
}else if (step.getActionKeyword().equalsIgnoreCase(AndroidActionTypeEnum.InitBaseDriverAndStartService.getActionKeyword())){
//获取上文AppiumServiceEntity启动AppiumDriverLocalService,根据用户配置APP相关参数初始化AndroidDriver
Method method = finalClazz.getMethod(step.getActionKeyword(), AppiumServiceEntity.class, String.class);
this.baseAndroidDriver = (AndroidDriver) method.invoke(instance,serviceEntity.get(),step.getParameter());
}else if (step.getActionKeyword().equalsIgnoreCase(AndroidActionTypeEnum.CloseAppiumService.getActionKeyword())){
//关闭AppiumDriverLocalService(先关闭AndroidDriver)
Method method = finalClazz.getMethod(step.getActionKeyword(),AppiumDriverLocalService.class);
if (serviceEntity.get().getService().isRunning()) {
reportLog.info("准备关闭Appium服务 ========== >> [{}]",serviceEntity.get().getService().getUrl());
this.baseAndroidDriver.quit();
reportLog.info(" ========== >> [已关闭baseAndroidDriver]");
method.invoke(instance,serviceEntity.get().getService());
}else {
throw new RuntimeException("CloseAppiumService失败! 不存在已启动的AppiumService");
}
}else {
//执行Android端操作关键字方法
invokeMethod(finalClazz, instance, this.baseAndroidDriver, step.getActionKeyword(), step.getElementLocateType(), step.getElementLocateValue(), step.getParameter());
}
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
});
});
}
private void invokeMethod(Class<?> clazz, Object instance, AndroidDriver driver, String actionKeyword, String elementLocateType, String elementLocateValue, String parameter) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
AndroidActionTypeEnum androidActionTypeEnum = AndroidActionTypeEnum.getTargetType(actionKeyword);
if (ObjectUtil.isNull(androidActionTypeEnum)) {
throw new IllegalStateException("操作编码 actionKeyword 不合法");
}
Method method;
switch (androidActionTypeEnum.getType()) {
case 0:
method = clazz.getMethod(actionKeyword,WebDriver.class);
method.invoke(instance,driver);
break;
case 1:
method = clazz.getMethod(actionKeyword,WebDriver.class,String.class);
method.invoke(instance,driver,parameter);
break;
case 2:
method = clazz.getMethod(actionKeyword,WebDriver.class,String.class,String.class);
method.invoke(instance,driver,elementLocateType,elementLocateValue);
break;
case 3:
method = clazz.getMethod(actionKeyword,WebDriver.class,String.class,String.class,String.class);
method.invoke(instance,driver,elementLocateType,elementLocateValue,parameter);
break;
case 4:
method = clazz.getMethod(actionKeyword,String.class);
method.invoke(instance,parameter);
break;
default:
break;
}
}
/**
* 在屏幕上画图标记(滑动操作)
* @param type 1-勾 2-叉 3-圆形
*/
protected void drawMarkInScreen(int type) {
drawMarkInScreen(baseAndroidDriver,type);
}
//此处省略各种具体关键字操作方法代码 与drawMarkInScreen(int type)类似 传入本基类baseAndroidDriver,调用AndroidCommon中方法执行具体关键字操作
/**
* 初始化 baseAndroidDriver
* @param capabilities
*/
protected void initBaseAndroidDriver(DesiredCapabilities capabilities) {
if (service == null) {
throw new RuntimeException("AppiumDriverLocalService service 未实例化!");
}
if (!service.isRunning()) {
throw new RuntimeException("AppiumDriverLocalService service 未运行!");
}
baseAndroidDriver = buildNewAndroidDriver(capabilities);
}
private AndroidDriver buildNewAndroidDriver(DesiredCapabilities capabilities) {
return new AndroidDriver(getServiceUrl(),capabilities);
}
}
-
Android测试通用-AndroidCommon
initBaseDriverAndStartService(AppiumServiceEntity serviceEntity, String androidInitInfos) //拿到App层创建的AppiumServiceEntity,开启服务,并初始化创建AndroidDriver
drawMarkInScreen(AndroidDriver driver,int type) //Android具体各自自处操作方法 此为屏幕上画标记 类似其他的方法 此处没展示,例如 点击、滑动、自定义滑动、缩放、扩放、拖拽等
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.nhdc.cloud.common.utils.CommonUtils;
import cn.nhdc.cloud.testscripts.config.AppiumServiceEntity;
import cn.nhdc.cloud.testscripts.config.Assertion;
import cn.nhdc.cloud.testscripts.config.ReportLog;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.appium.java_client.MultiTouchAction;
import io.appium.java_client.TouchAction;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.AndroidTouchAction;
import io.appium.java_client.android.nativekey.AndroidKey;
import io.appium.java_client.android.nativekey.KeyEvent;
import io.appium.java_client.service.local.AppiumDriverLocalService;
import io.appium.java_client.service.local.AppiumServerHasNotBeenStartedLocallyException;
import io.appium.java_client.touch.WaitOptions;
import io.appium.java_client.touch.offset.PointOption;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.Point;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.PointerInput;
import org.openqa.selenium.interactions.Sequence;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.Arrays;
public interface AndroidCommon extends AppCommon{
ReportLog reportLog = new ReportLog(AndroidCommon.class);
//以下为Excel自动化测试可调用
/**
* 在屏幕上画图标记(滑动操作)
* @param driver AndroidDriver
* @param type 1-勾 2-叉 3-圆形
*/
default void drawMarkInScreen(AndroidDriver driver,int type) {
AndroidTouchAction touchAction = new AndroidTouchAction(driver);
int width = driver.manage().window().getSize().width;
int height = driver.manage().window().getSize().height;
if (type < 1 || type > 3) type = RandomUtil.randomInt(1,4);
switch (type) {
case 1:
//打勾
touchAction.longPress(PointOption.point(width/4, height/2)).moveTo(PointOption.point(width/2,height*3/4)).moveTo(PointOption.point(width*3/4,height/4)).release().perform();
reportLog.info(" ======== >> 屏幕标记->[打勾]");
break;
case 2:
//画叉
touchAction.longPress(PointOption.point(width/4, height/3)).moveTo(PointOption.point(width*3/4,height*2/3)).release().perform();
touchAction.longPress(PointOption.point(width*3/4, height/3)).moveTo(PointOption.point(width/4,height*2/3)).release().perform();
reportLog.info(" ======== >> 屏幕标记->[画叉]");
break;
case 3:
//画圆
drawCircle(driver,new Point(width/2, height/2),200,30);
reportLog.info(" ======== >> 屏幕标记->[画圆]");
break;
default:
throw new IllegalArgumentException("标记类型不合法! ");
}
}
/**
* 绘制圆形
* @param driver
* @param origin 中心点位置
* @param radius 半径长度
* @param steps
*/
default void drawCircle (AndroidDriver driver, Point origin, double radius, int steps) {
Point firstPoint = getPointOnCircle(0, steps, origin, radius);
PointerInput finger = new PointerInput(PointerInput.Kind.TOUCH, "finger");
Sequence circle = new Sequence(finger, 0);
circle.addAction(finger.createPointerMove(Duration.ZERO, PointerInput.Origin.viewport(), firstPoint.x, firstPoint.y));
circle.addAction(finger.createPointerDown(PointerInput.MouseButton.LEFT.asArg()));
for (int i = 1; i < steps + 1; i++) {
Point point = getPointOnCircle(i, steps, origin, radius);
circle.addAction(finger.createPointerMove(Duration.ofMillis(100), PointerInput.Origin.viewport(), point.x, point.y));
}
circle.addAction(finger.createPointerUp(PointerInput.MouseButton.LEFT.asArg()));
driver.perform(Arrays.asList(circle));
}
default Point getPointOnCircle (int step, int totalSteps, Point origin, double radius) {
double theta = 2 * Math.PI * ((double)step / totalSteps);
int x = (int)Math.floor(Math.cos(theta) * radius);
int y = (int)Math.floor(Math.sin(theta) * radius);
return new Point(origin.x + x, origin.y + y);
}
//此处省略各种具体关键字操作方法代码 与drawMarkInScreen(AndroidDriver driver,int type)类似 具体各自实现
default AndroidDriver initBaseDriverAndStartService(AppiumServiceEntity serviceEntity, String androidInitInfos) {
if (ObjectUtil.isEmpty(serviceEntity)) {
throw new IllegalArgumentException("serviceEntity 不能为空!");
}
if (ObjectUtil.isEmpty(androidInitInfos)) {
throw new IllegalArgumentException("androidInitInfos 配置信息不能为空!");
}
if (!CommonUtils.isJSONString(androidInitInfos)) {
throw new IllegalArgumentException("androidInitInfos JSON 格式不合法! 请检查!");
}
DesiredCapabilities capabilities = new DesiredCapabilities();
JSONObject initJson = JSON.parseObject(androidInitInfos);
initJson.keySet().forEach(key -> capabilities.setCapability(key,initJson.get(key)));
URL url = null;
if (ObjectUtil.isNotEmpty(serviceEntity.getService())) {
AppiumDriverLocalService service = serviceEntity.getService();
try {
checkPortAndKillTask(APP_COMMON_DEFAULT_PORT);
service.start();
reportLog.info(" ========== >> 开启Appium本地服务 URL: [{}]",service.getUrl());
} catch (AppiumServerHasNotBeenStartedLocallyException e) {
e.printStackTrace();
}finally {
reportLog.info(" ========== >> Appium本地服务是否正常开启 [{}]",service.isRunning());
}
url = service.getUrl();
}else {
try {
url = new URL(serviceEntity.getUrl());
reportLog.info(" ========== >> 连接远程服务地址 [{}]",url);
} catch (MalformedURLException e) {
e.printStackTrace();
reportLog.info(" ========== >> 远程地址解析失败![{}]",serviceEntity);
}
}
return new AndroidDriver(url,capabilities);
}
以上核心逻辑,以及与Web端测试的区别已经基本介绍完了,很多关键字操作的具体实现代码没展示出来,因具体测试需求不同,实现方式不同,代码太杂乱不利于本次逻辑分享,有机会其他文章分享。
3、其他相同的以及相似不同但不重要得说明
- 完全相同的 TestCaseVo对象,解析Excel用例方法仍是
analysisExcelUiCase(String filePath, String sheetName, Integer headerRowNumber) - 相似的: 以下:
- Android操作关键字编码枚举
import io.appium.java_client.android.nativekey.AndroidKey;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.openqa.selenium.Keys;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Getter
@AllArgsConstructor
public enum AndroidActionTypeEnum {
//类型-0 无需定位 无需输入参数
ClosePage("关闭页面","closePage",0,"关闭当前所在页面窗口"),
CloseBrowser("关闭浏览器浏览器","closeBrowser",0,"关闭整个浏览器(所有页面)"),
PageBack("页面返回","pageBack",0,"页面返回"),
PageForward("页面前进","pageForward",0,"页面前进"),
PageRefresh("刷新页面","pageRefresh",0,"页面刷新"),
CloseAppiumService("关闭AppiumService","closeAppiumService",0,"关闭AppiumService 无输入值"),
ScaleScreen("双指触控缩放屏幕","scaleScreen",0,"双指触控缩放屏幕"),
EnlargeScreen("双指触控扩放屏幕","enlargeScreen",0,"双指触控扩放屏幕"),
SwipeDown("页面向下滑动","swipeDown",0,"页面向下滑动"),
SwipeUp("页面向上滑动","swipeUp",0,"页面向上滑动"),
SwipeLeft("页面向左滑动","swipeLeft",0,"页面向左滑动"),
SwipeRight("页面向右滑动","swipeRight",0,"页面向右滑动"),
//类型-1 无需定位 需输入参数
OpenUrl("打开网络地址","openUrl",1,"打开网址(输入值-网址)"),
NavigateToUrl("跳转网络地址","navigateToUrl",1,"(同一个页面窗口下)跳转至网址 (输入值-网址)"),
NavigateToWindows("跳转窗口至目标窗口","navigateToWindows",1,"跳转窗口至目标窗口(输入值-页面标题)"),
Pause("暂停N秒","pause",1,"暂停N秒(输入值-秒值 支持小数)"),
OpenUrlBlank("Blank方式(新开页面)打开网址","openUrlBlank",1,"Blank方式(新开页面)打开网址 (输入值-网址)"),
KeyBoard("输入键盘","keyBoard",1,"输入键盘(输入值 org.openqa.selenium.Keys枚举中的键盘值) 例如"+ Arrays.stream(Keys.values()).map(o -> o.name()).collect(Collectors.toList())),
Javascript("执行js脚本","javascript",1,"执行js脚本(输入值-js脚本)"),
PressKey("输入手机按键","pressKey",1,"输入手机按键(输入值-io.appium.java_client.android.nativekey.AndroidKey枚举中的按键值) 例如"+ Arrays.stream(AndroidKey.values()).map(o -> o.name()).collect(Collectors.toList())),
ClickText("点击目标文本元素","clickText",1,"点击目标文本元素(输入值-目标文本)"),
AssertToastHasAppeared("断言目标toast信息是否出现 (检查频率0.1秒) 供Excel自动化使用","assertToastHasAppeared",1,"断言目标toast信息是否出现 (检查频率0.1秒) 输入值格式: 等待时间:等待消息内容 例如 3:提交成功"),
DrawMarkInScreen("在屏幕上画图标记(滑动操作)","drawMarkInScreen",1,"在屏幕上画图标记(滑动操作) 输入值- 1-勾 2-叉 3-圆形"),
ClickRandomPointInCustomArea("给定区域内随机点击1次","clickRandomPointInCustomArea",1,"在给定区域内随机点击某点,输入值-左上角坐标:右下角坐标 坐标格式-X坐标,Y坐标 例如 200,200:300,300"),
ClickPoint("点击目标坐标点","clickPoint",1,"点击目标坐标点,输入值-X坐标:Y坐标 例如 200,200"),
SetTimeOut("设置全局隐式等待时间","setTimeOut",1,"设置全局隐式等待时间,输入值-秒值"),
//类型-2 需定位 无需输入参数
Click("点击","click",2,"点击(元素)"),
ClickNegatively("消极等待点击","clickNegatively",2,"消极等待点击(元素)"),
RightClick("右键单击元素","rightClick",2,"右键单击元素(元素)"),
MoveToElement("移动至元素","moveToElement",2,"焦点移动至(元素)"),
ClickAndHold("点击并保持按住","clickAndHold",2,"点击并保持按下状态(元素)"),
DoubleClick("双击","doubleClick",2,"双击(元素)"),
Release("释放元素","release",2,"释放(元素)"),
NavigateToFrame("跳转至Frame","navigateToFrame",2,"跳转Frame(元素)"),
//类型-3 需定位 需输入参数
ClickCustomWait("自定义等待点击","clickCustomWait",3,"自定义等待点击(元素)+ 输入值(时间配置 格式: 等待时间(单位秒):检查频率(单毫秒) 例如: 5:300 等待5秒每300毫秒检查一次)"),
ClickNegativelyCustomWait("自定义消极等待点击","clickNegativelyCustomWait",3,"自定义消极等待点击(元素)+ 输入值(时间配置 格式: 等待时间(单位秒):检查频率(单毫秒) 例如: 10:500 等待10秒每500毫秒检查一次)"),
InputText("输入文本","inputText",3,"输入文本(元素 + 输入值)"),
AssertElementText("断言类型-元素文本","assertElementText",3,"断言元素上文本是否符合预期 (元素 + 输入值-预期文本)"),
DragAndDropToPoint("拖拽某元素至目标坐标位置并释放","dragAndDropToPoint",3,"拖拽某元素至目标坐标位置并释放 (元素 +输入值-格式: x坐标:y坐标 例如:300:400 )"),
DragAndDropToElement("拖拽某元素至目标元素位置并释放","dragAndDropToElement",3,"拖拽某元素至目标元素位置并释放 (元素 + 输入值 定位方式|定位值 例如: xpath|//*[@id=\"pane-third\"]"),
ClickInElementsByText("点击组元素中文本符合预期的元素","clickInElementsByText",3,"点击组元素中文本符合预期的元素-(元素)"),
//类型-4 特殊类型 不需要WebDriver(其他类型需要WebDriver) 不需定位 传入需输入参数
ThreadSleep("线程休眠","threadSleep",4,"线程休眠(输入值 秒值-支持小数)"),
InitOrConnectAppiumService("根据用户配置 创建AppiumService或连接自定义的服务地址","initOrConnectAppiumService",4,"根据用户配置 是否创建AppiumService(输入值-JSON配置 配置说明: {\"isStartDefaultService\": true, #是否由服务器开启默认服务\"port\": 4721, # 如果不由服务器开启,则必须指定port\"ipAddress\": \"127.0.0.1\",# 如果不由服务器开启,则必须指定IP地址\"basePath\": \"/wd/hub/\" # 如果不由服务器开启,则必须指定basePath} "),
InitBaseDriverAndStartService("开启AppiumService并实例化Driver(打开APP)","initBaseDriverAndStartService",4,"开启AppiumService并实例化Driver(打开APP) (输入值-APP JOSN格式的DesiredCapabilities配置) 配置示例: {\"deviceName\":\"XXXXXXXXX\",\"platformName\":\"Android\",\"platformVersion\":\"10\",\"appPackage\":\"cn.*****.**\",\"appActivity\":\".ui.login.LaunchActivity\",\"automationName\":\"Appium\",\"chromedriverExecutable\":\"D:\\\\1111\\\\2222\\\\nhdc-222-test-platform\\\\src\\\\main\\\\resources\\\\driver\\\\chromedriver88.exe\",\"noReset\":\"false\",\"fastReset\":\"false\"}"),
;
private String name;
private String actionKeyword;
private Integer type;
private String description;
public static AndroidActionTypeEnum getTargetType(String actionKeyword) {
for (AndroidActionTypeEnum value : AndroidActionTypeEnum.values()) {
if (value.getActionKeyword().equalsIgnoreCase(actionKeyword)) {
return value;
}
}
return null;
}
public static List<String> getAllKeyword() {
return Arrays.stream(AndroidActionTypeEnum.values()).map(AndroidActionTypeEnum::getActionKeyword).collect(Collectors.toList());
}
public static List<String> getAllDescription() {
return Arrays.stream(AndroidActionTypeEnum.values()).map(AndroidActionTypeEnum::getDescription).collect(Collectors.toList());
}
}
- AndroidBasePage
import cn.nhdc.cloud.testscripts.config.ReportLog;
import cn.nhdc.cloud.testscripts.testcase.base.common.AndroidCommon;
import io.appium.java_client.android.Activity;
import io.appium.java_client.android.AndroidDriver;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
public class AndroidBasePage extends BasePage implements AndroidCommon {
private static final ReportLog reportLog = new ReportLog(AndroidBasePage.class);
public AndroidDriver driver;
public AndroidBasePage() {
}
public AndroidBasePage(AndroidDriver driver) {
this.driver = driver;
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(AndroidCommon.APP_COMMON_TIME_OUT));
}
public AndroidBasePage(AndroidDriver driver, String appPackage, String appActivity) {
this.driver = driver;
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(AndroidCommon.APP_COMMON_TIME_OUT));
this.driver.startActivity(new Activity(appPackage,appActivity));
}
public AndroidBasePage(AndroidDriver driver, String title) {
this.driver = driver;
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(AndroidCommon.APP_COMMON_TIME_OUT));
try {
boolean flag = wait.until((ExpectedCondition<Boolean>) arg0 -> arg0.getTitle().equals(title));
} catch (TimeoutException te) {
throw new IllegalStateException("当前不是预期页面,当前页面title是:" + driver.getTitle());
}
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(AndroidCommon.APP_COMMON_TIME_OUT));
}
}
6、其他相关
- 测试用例Excel: )
- Excel关键字描述: