今天简单介绍 移动端 UI 自动测试工具 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
任何 UI 自动化测试都不能完部替代人工测试,收益率高不高看部门怎么使用任何工具使用都是两方看怎么使用,如果有重复的工作每次需要人工去回归,建议使用自动化去回归,部门大家都用自动使用,会让大家的心信提高因为每次都机会使用自己写的脚本去验证自己重复工作。
脚本维护成本真的高吗?大家都说成本高,自己是否真的维护过,写过脚本?如果没有写过,没有维护过,没有发言权。只有自己用了才知道是否高。
打开下面链接选择版本为exe进行下载:
下载安装后:
点击启动:
安装JDK
下载地址:https://www.oracle.com/technetwork/java/javase/downloads/index.html
配置环境变量:
JAVA_HOME:
JAVA_HOME=C:\Program Files (x86)\Java\jdk1.8.0_181
%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;
CLASSPATH:
.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.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)
下载地址:
配置环境变量:
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);
/** * 购物车商品图片 * 长按 * @param driver */
public void cartSingleProductImage(AndroidDriver<AndroidElement> 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
/** * 滑动方法 * * @param driver * @param direction up、down、left、right */
static Duration duration = Duration.ofSeconds(1);
public static void swipe(AndroidDriver<AndroidElement> 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;
}
}
/** * 上滑 * * @param driver */
public static void SwipeUp(AndroidDriver<AndroidElement> 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();
}
/** * 下滑 * * @param driver */
public static void SwipeDown(AndroidDriver<AndroidElement> 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();
}
/** * 左滑 * * @param driver */
public static void SwipeLeft(AndroidDriver<AndroidElement> 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();
}
/** * 右滑 * * @param driver */
public static void SwipeRight(AndroidDriver<AndroidElement> 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();
}
获取属性:getAttribute()
获取文本:getText()
获取资源:getPageSource()
元素定位,两种方式。
第一种使用 sdk 中的【uiautomatorviewer.bat】进行元素定位
打开:
双击 uiautomatorviewer.bat
即可弹出:
在操作上面之前需要链接手机或者链接模拟器并操作命令显示:adb devices
如果是模拟器需要先链接:adb connect 127.0.0.1:62001
这样再次链接.
模拟器链接显示:
点击 sdk 中的【uiautomatorviewer.bat】
链接成功显示:
鼠标点击某个控件就会提示该控件可操作的相应内容:
说明:
其实在做移动端自动化测试,定位方式很少基本就是 id/name/xpath/ 坐标等定位方式。
driver.findElement(By.id("xxxxxx")).click();
driver.findElement(By.name("xxxxxx")).click();
xpath定位是最常用的一种方式,可以去学习下 xpath 语法:
但是网上也有大牛做一个插件,做 ui 自动化可直接使用:- https://github.com/lazytestteam/lazyuiautomatorviewer
大家下载后替换 sdk 中的 uiautomatorviewer.jar 就可使用。
点击 uiautomatorviewer.bat 再次弹出如下:
driver.findElement(By.xpath("xxxxxx")).click();
启动:
点击:
再弹出对话中输入:
在下面选项框中输入:
需要获取 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;
/** * @author 7DGroup * @Title: Bases * @Description: 安装初始化类 * @date 2019/11/20 / 22:34 */
public class DriverBase {
public static AndroidDriver<AndroidElement> driver;
/** * @param port :服务器启动的端口号,系统自动获取 * @param udid :手机设备号:系统自动化获取 * @param apk :自动化运行的APK包,系统会根据该地址获取包名与actiber * @param flag :true 卸掉有重新安装与运行后自动化卸掉包。false 直接安装运行 * @return */
public static AndroidDriver<AndroidElement> initDriver(String port, String udid, String apk, boolean flag) {
ArrayList<String> 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;
/** * @author 7DGroup * @Title: AndroidCapabilityType * @Description:功能配置类 * @date 2019/11/20 / 22:01 */
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;
/** * waitElement 时的最长等待时间 */
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";
/**adb*/
/** * 根据包名得到进程 */
public static final String GETAPPPACKAGEPID = "adb shell ps | grep ";
/** * 打开指定app */
public static final String OPEN_APP = "shell am start -n packagename activity";
/** * 本地存储截屏图片的目录,(注意配置时的格式) */
public static final String LOCAL_SCREEN_FILE_URL = getpathlocal();
/** * 获取目录工程路径 * * @return */
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:
/** * 获取包名与 APP_ACTIVITY * * @param path * @return */
public static ArrayList<String> getPackAct(String path) {
ArrayList<String> list = new ArrayList<>();
try {
List<String> 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;
}
测试报告:
部分代码(如果需要请再群@)
/** * @author 7DGroup * @Title: ReporterListener * @Description: 自定义报告监听类 * @date 2019/11/21 / 18:56 */
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<XmlSuite> xmlSuites, List<ISuite> suites, String outputDirectory) {
List<ITestResult> 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<String, ISuiteResult> 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<String, TestResultCollection> 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);
}
/** * 输出模板 * * @param context * @param 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();
}
}
/** * 排序规则 * * @param list */
private void sort(List<ITestResult> list) {
Collections.sort(list, new Comparator<ITestResult>() {
@Override
public int compare(ITestResult r1, ITestResult r2) {
if (r1.getStatus() < r2.getStatus()) {
return 1;
} else {
return -1;
}
}
});
}
模板(部分代码):
<h2>详情</h2>
#foreach($result in $results.entrySet())
#set($item = $result.value)
<table id="$result.key" class="details">
<tr>
<th>测试类</th>
<td colspan="4">$result.key</td>
</tr>
<tr>
<td>TOTAL: $item.totalSize</td>
<td>SUCCESS: $item.successSize</td>
<td>FAILED: $item.failedSize</td>
<td>ERROR: $item.errorSize</td>
<td>SKIPPED: $item.skippedSize</td>
</tr>
<tr>
<th>Status</th>
<th>Method</th>
<th>Description</th>
<th>Duration</th>
<th>Detail</th>
</tr>
#foreach($testResult in $item.resultList)
<tr>
#if($testResult.status==1)
<th class="success" style="width:5em;">success
</td>
#elseif($testResult.status==2)
<th class="failure" style="width:5em;">failure
</td>
#elseif($testResult.status==3)
<th class="skipped" style="width:5em;">skipped
</td>
#end
<td>$testResult.testName</td>
<td>${testResult.description}</td>
<td>${testResult.duration} seconds</td>
<td class="detail">
## <a class="button" href="#popup_log_${testResult.caseName}_${testResult.testName}">log</a>
<button type="button" class="btn btn-primary btn-lg" data-toggle="modal" data-target="#popup_log_${testResult.caseName}_${testResult.testName}">
log
</button>
<!-- 日志模态框 -->
<div class="modal fade" id="popup_log_${testResult.caseName}_${testResult.testName}" tabindex="-1" role="dialog" aria-labelledby="myModalLabel_${testResult.testName}">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel_${testResult.testName}">用例操作步骤</h4>
</div>
<div class="modal-body">
<div style="overflow: auto">
<table>
<tr>
<th>日志</th>
<td>
#foreach($msg in $testResult.twooutparam)
<pre>$msg</pre>
#end
</td>
</tr>
#if($testResult.status==2)
<tr>
<th>异常</th>
<td>
<pre>$testResult.throwableTrace</pre>
</td>
</tr>
#end
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</td>
</tr>
#end
</table>
#end
启动测试类:
static AndroidDriver<AndroidElement> driver;
/** * 初始化运行类 * * @param udid * @param port * @throws Exception */
@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 version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
<suite name="UI自动化" parallel="tests" thread-count="1">
<listeners>
<listener class-name="appout.reporter.ReporterListener"></listener>
</listeners>
<test name="M6TGLMA721108530">
<parameter name="udid" value="M6TGLMA721108530"/>
<parameter name="port" value="4723"/>
<classes>
<class name="appout.appcase.LoginTest"/>
</classes>
</test>
</suite>
命令号启动:
这样跑xml就能得到如下结果。
log 弹出:
注意:
如果在启动的时候有问题,自己微调下,大概大家只是看看而已,有问题到群里问或者联系@就行会单独指导怎么使用。
使用 maven 建立项目,通过 tesng 做测试类与传参,以上简单介绍了环境部署,定位方式,启动类,报告类等方法。
主要的知识点:
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/zuozewei/article/details/120595318
内容来源于网络,如有侵权,请联系作者删除!