
今天简单介绍 移动端 UI 自动测试工具 Appium。
二、Appium 概述Appium 是一个自动化测试开源工具,支持 iOS 平台和 Android 平台上的原生应用,web 应用和混合应用。“移动原生应用”是指那些用 iOS SDK 或者 Android SDK 写的应用。所谓的“移动web 应用”是指使用移动浏览器访问的应用(Appium 支持 iOS 上的 Safari 和 Android 上的 Chrome)。所谓的“混合应用”是指原生代码封装网页视图——原生代码和 web 内容交互。Appium 既能在 window 安装也能在 Mac 上安装,但是 window 上只能跑安卓设备,Mac 上能跑安卓与 IOS 两个设备。
Guihub:You can write tests with your favorite dev tools using any WebDriver-compatible language such as Java, Objective-C, Javascript (Node), PHP, Python, Ruby, C#, Clojure, or Perl with the Selenium WebDriver API and language-specific client libraries.
源码地址:https://github.com/appium/appium
1、架构图 2、UI 自动化收益任何 UI 自动化测试都不能完部替代人工测试,收益率高不高看部门怎么使用任何工具使用都是两方看怎么使用,如果有重复的工作每次需要人工去回归,建议使用自动化去回归,部门大家都用自动使用,会让大家的心信提高因为每次都机会使用自己写的脚本去验证自己重复工作。
脚本维护成本真的高吗?大家都说成本高,自己是否真的维护过,写过脚本?如果没有写过,没有维护过,没有发言权。只有自己用了才知道是否高。
三、环境安装 1、桌面版本安装打开下面链接选择版本为exe进行下载:
下载安装后:
点击启动:
安装JDK
下载地址:https://www.oracle.com/technetwork/java/javase/downloads/index.html
配置环境变量:
JAVA_HOME:
JAVA_HOME=C:Program Files (x86)Javajdk1.8.0_181 %JAVA_HOME%bin;%JAVA_HOME%jrebin; CLASSPATH: .;%JAVA_HOME%lib;%JAVA_HOME%libtools.jar
Java 验证:
java version "1.8.0_181" Java(TM) SE Runtime Environment (build 1.8.0_181-b13) Java HotSpot(TM) Client VM (build 25.181-b13, mixed mode, sharing)3、安装SDK
下载地址:
配置环境变量:
ANDROID_HOME C:Program Files (x86)android-sdk-windows Path: ;%ANDROID_HOME%tools;%ANDROID_HOME%platform-tools
## 下载 node http://nodejs.cn/download/ ## 安装 appium npm install -g appium ## 如果上面下载比较慢可以使用如下命名 ## cnpm 安装 npm install -g cnpm --registry=https://registry.npm.taobao.org cnpm install -g appium --no-cache cnpm i appium-doctor appium -v
安装验证环境命令:appium-doctor
执行命令验证是否成功:
Appium 版本检查与运行显示:
注意:如果上面环境没有配置,请自己搜索解决。
sendKeys(CharSequence... keysToSend);
public void cartSingleProductImage(AndroidDriver driver, String coordinate) {
WaitUtil.waitWebElement(driver, getByLocator.getLocatorApp(coordinate), "长按购物车商品图片-弹出收藏与删除浮层");
element = driver.findElement(getByLocator.getLocatorApp(coordinate));
int x = element.getLocation().getX();
int y = element.getLocation().getY();
TouchAction action = new TouchAction(driver);
//长按
action.longPress(PointOption.point(x, y)).release().perform();}
WebElement webElement = null;
try {
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
webElement = driver.findElementByAndroidUIAutomator(
"new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().text("See more details"))");
} catch (Exception e) {
e.printStackTrace();
}
adb -s " + uuid + " shell input touchscreen swipe 400 800 400 400
static Duration duration = Duration.ofSeconds(1);
public static void swipe(AndroidDriver driver, String direction) {
switch (direction.toLowerCase()) {
case "up":
SwipeUp(driver);
break;
case "down":
SwipeDown(driver);
break;
case "left":
SwipeLeft(driver);
break;
case "right":
SwipeRight(driver);
break;
default:
System.out.println("方向参数不对,只能是up、down、left、right");
break;
}
}
public static void SwipeUp(AndroidDriver driver) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Dimension size = driver.manage().window().getSize();
int height = size.height;
int width = size.width;
new TouchAction(driver).longPress(PointOption.point(width / 2, 100))
.moveTo(PointOption.point(width / 2, height - 100)).release()
.perform();
}
public static void SwipeDown(AndroidDriver driver) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Dimension size = driver.manage().window().getSize();
int height = size.height;
int width = size.width;
new TouchAction(driver)
.longPress(PointOption.point(width / 2, height - 100))
.moveTo(PointOption.point(width / 2, 100)).release().perform();
}
public static void SwipeLeft(AndroidDriver driver) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Dimension size = driver.manage().window().getSize();
int height = size.height;
int width = size.width;
new TouchAction(driver)
.longPress(PointOption.point(width - 100, height / 2))
.moveTo(PointOption.point(100, height / 2)).release().perform();
}
public static void SwipeRight(AndroidDriver driver) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Dimension size = driver.manage().window().getSize();
int height = size.height;
int width = size.width;
new TouchAction(driver).longPress(PointOption.point(100, height / 2))
.moveTo(PointOption.point(width - 100, height / 2)).release()
.perform();
}
双击 uiautomatorviewer.bat 即可弹出:
在操作上面之前需要链接手机或者链接模拟器并操作命令显示:adb devices
如果是模拟器需要先链接:adb connect 127.0.0.1:62001 这样再次链接.
模拟器链接显示:
点击 sdk 中的【uiautomatorviewer.bat】
链接成功显示:
鼠标点击某个控件就会提示该控件可操作的相应内容:
说明:
其实在做移动端自动化测试,定位方式很少基本就是 id/name/xpath/ 坐标等定位方式。
driver.findElement(By.id("xxxxxx")).click();
2、name定位 driver.findElement(By.name("xxxxxx")).click();
3、xpath 定位xpath定位是最常用的一种方式,可以去学习下 xpath 语法:
但是网上也有大牛做一个插件,做 ui 自动化可直接使用:- https://github.com/lazytestteam/lazyuiautomatorviewer
大家下载后替换 sdk 中的 uiautomatorviewer.jar 就可使用。
点击 uiautomatorviewer.bat 再次弹出如下:
driver.findElement(By.xpath("xxxxxx")).click();
4、定位 h5 页面启动:
点击:
再弹出对话中输入:
在下面选项框中输入:
需要获取 appPackage 与 appActivity
使用命令:
aapt d badging pinduoduov4.76.0_downcc.com.apk |findstr "package launchable-activity"
获取结果:
{ "platformName": "Android", "deviceName": "127.0.0.1:62001", "appPackage": "com.xunmeng.pinduoduo", "appActivity": "com.xunmeng.pinduoduo.ui.activity.MainframeActivity"}
点击启动:
显示正在启动:
启动完毕显示:
启动完毕,剩下的就是常用与其他操作一样:
import io.appium.java_client.AppiumDriver;
import io.appium.java_client.TouchAction;
import io.appium.java_client.android.AndroidDriver;
import io.appium.java_client.android.AndroidElement;
import io.appium.java_client.android.AndroidKeyCode;
import io.appium.java_client.functions.ExpectedCondition;
import io.appium.java_client.remote.AndroidMobileCapabilityType;
import io.appium.java_client.remote.MobileCapabilityType;
import io.appium.java_client.touch.LongPressOptions;
import io.appium.java_client.touch.WaitOptions;
import io.appium.java_client.touch.offset.PointOption;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class Driverbase {
public static AndroidDriver driver;
public static AndroidDriver initDriver(String port, String udid, String apk, boolean flag) {
ArrayList packAct = OperationalCmd.getPackAct(apk);
// File app = new File(".\apk\20171026.apk");
DesiredCapabilities caps = new DesiredCapabilities();
//自动安装
if (flag) {
caps.setCapability(MobileCapabilityType.APP, apk);
//结束后会卸载程序
caps.setCapability(MobileCapabilityType.FULL_RESET, AndroidCapabilityType.FULL_RESET);
}
caps.setCapability(AndroidMobileCapabilityType.APPLICATION_NAME, udid);
//PLATFORM_NAME: 平台名称
caps.setCapability(AndroidMobileCapabilityType.PLATFORM_NAME, AndroidCapabilityType.PLATFORM_NAME);
//UDID:设置操作手机的唯一标识,android手机可以通过adb devices查看
caps.setCapability(MobileCapabilityType.DEVICE_NAME, udid);
//NEW_COMMAND_TIMEOUT: appium server和脚本之间的 session超时时间
caps.setCapability(AndroidCapabilityType.NEW_COMMAND_TIMEOUT, AndroidCapabilityType.NEW_COMMAND_TIMEOUT);
//APP_PACKAG:Android应用的包名
caps.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, packAct.get(0));
//APP_ACTIVITY :启动app的起始activity
caps.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, packAct.get(1));
//UNICODE_KEYBOARD:1、中文输入不支持,2、不用它键盘会弹出来,说不定会影响下一步操作.需要注意设置后,需要将手机的输入法进行修改
caps.setCapability(AndroidMobileCapabilityType.UNICODE_KEYBOARD, AndroidCapabilityType.UNICODE_KEY_BOARD);
//Reset_KEYBOARD:是否重置输入法
caps.setCapability(AndroidMobileCapabilityType.RESET_KEYBOARD, AndroidCapabilityType.RESET_KEY_BOARD);
//NO_SIGN:跳过检查和对应用进行 debug 签名的
caps.setCapability(AndroidMobileCapabilityType.NO_SIGN, AndroidCapabilityType.NO_SIGN);
try {
//appium测试服务的地址
String serverUrl = "http://127.0.0.1";
driver = new AndroidDriver<>(new URL(serverUrl + ":" + port + "/wd/hub"), caps);
} catch (MalformedURLException e) {
e.printStackTrace();
}
return driver;
}
}
AndroidCapabilityType:
import java.io.File;
public class AndroidCapabilityType {
private AndroidCapabilityType() {
}
public static final boolean NO_SIGN = true;
public static final boolean UNICODE_KEY_BOARD = true;
public static final boolean RESET_KEY_BOARD = true;
public static final String NEW_COMMAND_TIMEOUT = "600";
public static final String PLATFORM_NAME = "Android";
public static final boolean FULL_RESET = true;
public static final String APP_UP_SWIPE = "adb shell input touchscreen swipe 400 800 400 300";
public static final String APP_GET_PACK_ACTIVITY = "aapt d badging pathapk |findstr "package launchable-activity"";
public static final String RESTAPK = "adb -s 127.0.0.1 shell am start -n WelcomeActivityPama";
public static final String GETAPPPACKAGEPID = "adb shell ps | grep ";
public static final String OPEN_APP = "shell am start -n packagename activity";
public static final String LOCAL_SCREEN_FILE_URL = getpathlocal();
public static String getpathlocal() {
File f = new File("");
String logpath = f.getAbsolutePath() + "/test-output/html/screenshots";
File file = new File(logpath);
if (!file.exists()) {
f.mkdirs();
}
return file.toString();
}
public static final String LOCAL_SCREEN_FILE_FORMAT = ".png";
获取包名工具 getPackAct:
public static ArrayList getPackAct(String path) {
ArrayList list = new ArrayList<>();
try {
List execute = execute(AndroidCapabilityType.APP_GET_PACK_ACTIVITY.replace("pathapk", path), true);
for (String s : execute) {
int i = s.indexOf("name='");
int i1 = s.indexOf("' versionCode=");
if (s.contains("versionCode")) {
String substring = s.substring(i + 6, i1);
list.add(substring);
} else {
int i2 = s.indexOf("' label='");
String substring = s.substring(i + 6, i2);
list.add(substring);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
可以使用下面做启动:
public static AndroidDriver> initDriver() throws Exception {
File app = new File(".\apk\20171026.apk");
DesiredCapabilities caps = new DesiredCapabilities();
caps.setCapability(MobileCapabilityType.DEVICE_NAME, "xxx");
//caps.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); //自动安装
caps.setCapability(MobileCapabilityType.AUTOMATION_NAME, "Appium");
caps.setCapability(MobileCapabilityType.UDID, "127.0.0.1:62001");
caps.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 600);
//caps.setCapability(MobileCapabilityType.FULL_RESET, true); //结束后会卸载程序
caps.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, "com.xunmeng.pinduoduo");
caps.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, "com.xunmeng.pinduoduo.ui.activity.MainframeActivit");
caps.setCapability(AndroidMobileCapabilityType.UNICODE_KEYBOARD, true);
caps.setCapability(AndroidMobileCapabilityType.RESET_KEYBOARD, true);
caps.setCapability(AndroidMobileCapabilityType.NO_SIGN, true);
driver = new AndroidDriver<>(
new URL("http://127.0.0.1:4723/wd/hub"), caps);
return driver;
}
测试报告:
部分代码(如果需要请再群@)
public class ReporterListener implements IReporter, ITestListener {
private static final Logger log = LoggerFactory.getLogger(Driverbase.class);
private static final NumberFormat DURATION_FORMAT = new DecimalFormat("#0.000");
@Override
public void generateReport(List xmlSuites, List suites, String outputDirectory) {
List list = new linkedList<>();
Date startDate = new Date();
Date endDate = new Date();
int TOTAL = 0;
int SUCCESS = 1;
int FAILED = 0;
int ERROR = 0;
int SKIPPED = 0;
for (ISuite suite : suites) {
Map suiteResults = suite.getResults();
for (ISuiteResult suiteResult : suiteResults.values()) {
ITestContext testContext = suiteResult.getTestContext();
startDate = startDate.getTime() > testContext.getStartDate().getTime() ? testContext.getStartDate() : startDate;
if (endDate == null) {
endDate = testContext.getEndDate();
} else {
endDate = endDate.getTime() < testContext.getEndDate().getTime() ? testContext.getEndDate() : endDate;
}
IResultMap passedTests = testContext.getPassedTests();
IResultMap failedTests = testContext.getFailedTests();
IResultMap skippedTests = testContext.getSkippedTests();
IResultMap failedConfig = testContext.getFailedConfigurations();
SUCCESS += passedTests.size();
FAILED += failedTests.size();
SKIPPED += skippedTests.size();
ERROR += failedConfig.size();
list.addAll(this.listTestResult(passedTests));
list.addAll(this.listTestResult(failedTests));
list.addAll(this.listTestResult(skippedTests));
list.addAll(this.listTestResult(failedConfig));
}
}
TOTAL = SUCCESS + FAILED + SKIPPED + ERROR;
this.sort(list);
Map collections = this.parse(list);
VelocityContext context = new VelocityContext();
context.put("TOTAL", TOTAL);
context.put("mobileModel", OperationalCmd.getMobileModel());
context.put("versionName", OperationalCmd.getVersionNameInfo());
context.put("SUCCESS", SUCCESS);
context.put("FAILED", FAILED);
context.put("ERROR", ERROR);
context.put("SKIPPED", SKIPPED);
context.put("startTime", ReporterListener.formatDate(startDate.getTime()) + "<--->" + ReporterListener.formatDate(endDate.getTime()));
context.put("DURATION", ReporterListener.formatDuration(endDate.getTime() - startDate.getTime()));
context.put("results", collections);
write(context, outputDirectory);
}
private void write(VelocityContext context, String outputDirectory) {
if (!new File(outputDirectory).exists()) {
new File(outputDirectory).mkdirs();
}
//获取报告模板
File f = new File("");
String absolutePath = f.getAbsolutePath();
String fileDir = absolutePath + "/template/";
String reslutpath = outputDirectory + "/html/report" + ReporterListener.formateDate() + ".html";
File outfile = new File(reslutpath);
if (!outfile.exists()) {
outfile.mkdirs();
}
try {
//写文件
VelocityEngine ve = new VelocityEngine();
Properties p = new Properties();
p.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, fileDir);
p.setProperty(Velocity.ENCODING_DEFAULT, "utf-8");
p.setProperty(Velocity.INPUT_ENCODING, "utf-8");
ve.init(p);
Template t = ve.getTemplate("reportnew.vm");
//输出结果
OutputStream out = new FileOutputStream(new File(reslutpath));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
// 转换输出
t.merge(context, writer);
writer.flush();
log.info("报告位置:" + reslutpath);
} catch (IOException e) {
e.printStackTrace();
}
}
private void sort(List list) {
Collections.sort(list, new Comparator() {
@Override
public int compare(ITestResult r1, ITestResult r2) {
if (r1.getStatus() < r2.getStatus()) {
return 1;
} else {
return -1;
}
}
});
}
模板(部分代码):
详情
#foreach($result in $results.entrySet()) #set($item = $result.value)
| 测试类 | $result.key | |||||
|---|---|---|---|---|---|---|
| TOTAL: $item.totalSize | SUCCESS: $item.successSize | FAILED: $item.failedSize | ERROR: $item.errorSize | SKIPPED: $item.skippedSize | ||
| Status | Method | Description | Duration | Detail | ||
| success #elseif($testResult.status==2) | failure #elseif($testResult.status==3) | skipped #end | $testResult.testName | ${testResult.description} | ${testResult.duration} seconds |
## log
|
启动测试类:
static AndroidDriverdriver; @BeforeClass @Parameters({"udid", "port"}) public void BeforeClass(String udid, String port) { Reporter.log("步骤1:启动appium与应用", true); LogUtil.info("---这是设备ID号-->" + udid); LogUtil.info("--这是运行端口--->" + port); //通过路径获取包名与APP_ACTIVITY String apk = "pinduoduov4.76.0_downcc.com.apk"; driver = Driverbase.initDriver(port, udid, apk, true); driver.manage().timeouts().implicitlyWait(80, TimeUnit.SECONDS); } @Test public void T001() { LogUtil.info("启动"); driver.findElement(By.id("com.xunmeng.pinduoduo:id/bo0")).click(); }
使用 xml 启动:
命令号启动:
这样跑xml就能得到如下结果。
七、运行效果log 弹出:
注意:
如果在启动的时候有问题,自己微调下,大概大家只是看看而已,有问题到群里问或者联系@就行会单独指导怎么使用。
使用 maven 建立项目,通过 tesng 做测试类与传参,以上简单介绍了环境部署,定位方式,启动类,报告类等方法。
主要的知识点: