一、前言
今天简单介绍 移动端 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进行下载:
下载安装后: ?
点击启动:
2、DOS命令安装
? 安装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)
3、安装SDK
下载地址:
配置环境变量:
ANDROID_HOME
C:\Program Files (x86)\android-sdk-windows
Path:
;%ANDROID_HOME%\tools;%ANDROID_HOME%\platform-tools
http://nodejs.cn/download/
npm install -g appium
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<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();
}
- 上下左右滑动,最简单的下滑命令行:(uuid 表示手机设备号)
adb -s " + uuid + " shell input touchscreen swipe 400 800 400 400
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;
}
}
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();
}
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();
}
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();
}
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() - 获取资源:g
etPageSource() - 元素定位,两种方式。
- 第一种使用 sdk 中的【uiautomatorviewer.bat】进行元素定位
打开:
双击 uiautomatorviewer.bat 即可弹出: ?
在操作上面之前需要链接手机或者链接模拟器并操作命令显示:adb devices 如果是模拟器需要先链接:adb connect 127.0.0.1:62001 这样再次链接.
模拟器链接显示:
点击 sdk 中的【uiautomatorviewer.bat】
链接成功显示:
鼠标点击某个控件就会提示该控件可操作的相应内容:
说明:
其实在做移动端自动化测试,定位方式很少基本就是 id/name/xpath/ 坐标等定位方式。 ?
五、常见定位方式
1、ID 定位
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"}
?
点击启动: ?
显示正在启动:
启动完毕显示:
启动完毕,剩下的就是常用与其他操作一样: ?
六、简单 demo 示例
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<AndroidElement> driver;
public static AndroidDriver<AndroidElement> initDriver(String port, String udid, String apk, boolean flag) {
ArrayList<String> packAct = OperationalCmd.getPackAct(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);
caps.setCapability(AndroidMobileCapabilityType.PLATFORM_NAME, AndroidCapabilityType.PLATFORM_NAME);
caps.setCapability(MobileCapabilityType.DEVICE_NAME, udid);
caps.setCapability(AndroidCapabilityType.NEW_COMMAND_TIMEOUT, AndroidCapabilityType.NEW_COMMAND_TIMEOUT);
caps.setCapability(AndroidMobileCapabilityType.APP_PACKAGE, packAct.get(0));
caps.setCapability(AndroidMobileCapabilityType.APP_ACTIVITY, packAct.get(1));
caps.setCapability(AndroidMobileCapabilityType.UNICODE_KEYBOARD, AndroidCapabilityType.UNICODE_KEY_BOARD);
caps.setCapability(AndroidMobileCapabilityType.RESET_KEYBOARD, AndroidCapabilityType.RESET_KEY_BOARD);
caps.setCapability(AndroidMobileCapabilityType.NO_SIGN, AndroidCapabilityType.NO_SIGN);
try {
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<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.AUTOMATION_NAME, "Appium");
caps.setCapability(MobileCapabilityType.UDID, "127.0.0.1:62001");
caps.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 600);
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<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);
}
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<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;
@BeforeClass
@Parameters({"udid", "port"})
public void BeforeClass(String udid, String port) {
Reporter.log("步骤1:启动appium与应用", true);
LogUtil.info("---这是设备ID号-->" + udid);
LogUtil.info("--这是运行端口--->" + port);
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 做测试类与传参,以上简单介绍了环境部署,定位方式,启动类,报告类等方法。
主要的知识点:
?
?
?
?
?
?
?
|