2021年(31)
分类: Java
2021-05-14 17:03:45
自动化,性能测试,接口测试,开发平台等工作,到底测试的价值在哪里,其实价值来源不断充实与为大众服务,今天简单介绍ui小工具appium攻击。
Appium 是一个自动化测试开源工具,支持 iOS 平台和 Android 平台上的原生应用,web 应用和混合应用。“移动原生应用”是指那些用 iOS SDK 或者 Android SDK 写的应用。所谓的“移动web 应用”是指使用移动浏览器访问的应用(Appium 支持 iOS 上的 Safari 和 Android 上的 Chrome)。所谓的“混合应用”是指原生代码封装网页视图——原生代码和 web 内容交互。Appium既能在window安装也能在mac上安装,但是wind上只能跑安卓设备,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.
来自网络
任何UI自动测试都不能完部替代人工测试,收益率高不高看部门怎么使用任何工具使用都是两方看怎么使用,如果有重复的工作每次需要人工去回归,建议使用自动化去回归,部门大家都用自动使用,会让大家的心信提高因为每次都机会使用自己写的脚本去验证自己重复工作。
脚本维护成本真的高吗?大家都说成本高,自己是否真的维护过,写过脚本?如果没有写过,没有维护过,没有发言权。只有自己用了才知道是否高。
今天咱们使用windos搭建appium自动化,使用java语言做脚本语言,内容会简单介绍安装,定位,使用还有简单框架跑起来,为了节约大家时间先告诉大家本文文章主要内容是什么,这样方便是否选择看下去。希望对没有做自动化的一点启示。
环境安装 Android
常用操作
元素定位(原生)
简单java demo使用
简单框架设计
报告二次封装
打开下面链接选择版本为exe进行下载:
https://github.com/appium/appium-desktop/releases
下载安装后
点击启动:
DOS命令安装
下载:
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 -version验证:
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)
https://android-sdk.en.softonic.com/
https://android-sdk.en.softonic.com/download
配置环境变量:
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
如果上面下载比较慢可以使用如下命名:
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
注意:如果上面环境没有配置,请百度自己查询
点击
click()
输入
sendKeys(CharSequence... keysToSend);
清除
clear()
长按
/** * 购物车商品图片 * 长按 * @param driver */ public void cartSingleProductImage(AndroidDriverdriver, 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
/**
* 滑动方法
*
* @param driver
* @param direction up、down、left、right
*/
staticDuration duration =Duration.ofSeconds(1);
publicstaticvoid 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
*/
publicstaticvoidSwipeUp(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;
newTouchAction(driver).longPress(PointOption.point(width /2,100))
.moveTo(PointOption.point(width /2, height -100)).release()
.perform();
}
/**
* 下滑
*
* @param driver
*/
publicstaticvoidSwipeDown(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;
newTouchAction(driver)
.longPress(PointOption.point(width /2, height -100))
.moveTo(PointOption.point(width /2,100)).release().perform();
}
/**
* 左滑
*
* @param driver
*/
publicstaticvoidSwipeLeft(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;
newTouchAction(driver)
.longPress(PointOption.point(width -100, height /2))
.moveTo(PointOption.point(100, height /2)).release().perform();
}
/**
* 右滑
*
* @param driver
*/
publicstaticvoidSwipeRight(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;
newTouchAction(driver).longPress(PointOption.point(100, height /2))
.moveTo(PointOption.point(width -100, height /2)).release()
.perform();
}
获取属性
getAttribute()
获取文本
getText()
获取资源
getPageSource()
元素定位
两种方式:一种使用skd中的【uiautomatorviewer.bat】进行元素定位
打开:
双击uiautomatorviewer.bat即可弹出:
在操作上面之前需要链接手机或者链接模拟器并操作命令显示:adb devices
如果是模拟器需要先链接:adb connect 127.0.0.1:62001这样再次链接
模拟器链接显示:
链接成功显示:
上面操作说明:
鼠标点击某个控件就会提示该控件可操作的相应内容:
说明:
其实在做移动端自动化测试,定位方式很少基本就是id/name/xpath/坐标等定位方式:
driver.findElement(By.id("xxxxxx")).click();
driver.findElement(By.name("xxxxxx")).click();
xpath定位是最常用的一种方式,可以去学习下xpath语法:https://www.w3school.com.cn/xpath/xpath_syntax.asp 但是网上也有大牛做一个插件,做ui自动化可直接使用:https://github.com/lazytestteam/lazyuiautomatorviewer 大家下载后替换sdk中的uiautomatorviewer.jar就可使用,点击 uiautomatorviewer.bat再次弹出如下:
driver.findElement(By.xpath("xxxxxx")).click();
目前这中方式是可以定位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"}
点击启动
显示正在启动
启动完毕显示:
启动完毕,剩下的就是常用与其他操作一样
简单java->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;
/**
* @author liwen
* @Title: Bases
* @Description: 安装初始化类
* @date 2019/11/20 / 22:34
*/
publicclassDriverBase{
publicstaticAndroidDriver<AndroidElement> driver;
/**
* @param port :服务器启动的端口号,系统自动获取
* @param udid :手机设备号:系统自动化获取
* @param apk :自动化运行的APK包,系统会根据该地址获取包名与actiber
* @param flag :true 卸掉有重新安装与运行后自动化卸掉包。false 直接安装运行
* @return
*/
publicstaticAndroidDriver<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 =newDesiredCapabilities();
//自动安装
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 =newAndroidDriver<>(new URL(serverUrl +":"+ port +"/wd/hub"), caps);
}catch(MalformedURLException e){
e.printStackTrace();
}
return driver;
}
}
import java.io.File;
/**
* @author liwen
* @Title: AndroidCapabilityType
* @Description:功能配置类
* @date 2019/11/20 / 22:01
*/
publicclassAndroidCapabilityType{
privateAndroidCapabilityType(){
}
publicstaticfinalboolean NO_SIGN =true;
publicstaticfinalboolean UNICODE_KEY_BOARD =true;
publicstaticfinalboolean RESET_KEY_BOARD =true;
/**
* waitElement 时的最长等待时间
*/
publicstaticfinalString NEW_COMMAND_TIMEOUT ="600";
publicstaticfinalString PLATFORM_NAME ="Android";
publicstaticfinalboolean FULL_RESET =true;
/**
* 向上小滑动一步
*/
publicstaticfinalString APP_UP_SWIPE ="adb shell input touchscreen swipe 400 800 400 300";
publicstaticfinalString APP_GET_PACK_ACTIVITY ="aapt d badging pathapk |findstr \"package launchable-activity\"";
/**
* 重启应用
*/
publicstaticfinalString RESTAPK ="adb -s 127.0.0.1 shell am start -n WelcomeActivityPama";
/**adb*/
/**
* 根据包名得到进程
*/
publicstaticfinalString GETAPPPACKAGEPID ="adb shell ps | grep ";
/**
* 打开指定app
*/
publicstaticfinalString OPEN_APP ="shell am start -n packagename activity";
/**
* 本地存储截屏图片的目录,(注意配置时的格式)
*/
publicstaticfinalString LOCAL_SCREEN_FILE_URL = getpathlocal();
/**
* 获取目录工程路径
*
* @return
*/
publicstaticString getpathlocal(){
File f =newFile("");
String logpath = f.getAbsolutePath()+"/test-output/html/screenshots";
File file =newFile(logpath);
if(!file.exists()){
f.mkdirs();
}
return file.toString();
}
/**
* 本地存储截屏图片的格式
*/
publicstaticfinalString LOCAL_SCREEN_FILE_FORMAT =".png";
/** * 获取包名与 APP_ACTIVITY * * @param path * @return */ public static ArrayListgetPackAct(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; }
部分代码(如果需要请再群@)
/**
* @author liwen
* @Title: ReporterListener
* @Description: 自定义报告监听类
* @date 2019/11/21 / 18:56
*/
publicclassReporterListenerimplementsIReporter,ITestListener{
privatestaticfinalLogger log =LoggerFactory.getLogger(DriverBase.class);
privatestaticfinalNumberFormat DURATION_FORMAT =newDecimalFormat("#0.000");
@Override
publicvoid generateReport(List<XmlSuite> xmlSuites,List<ISuite> suites,String outputDirectory){
List<ITestResult> list =newLinkedList<>();
Date startDate =newDate();
Date endDate =newDate();
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 =newVelocityContext();
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
*/
privatevoid write(VelocityContext context,String outputDirectory){
if(!newFile(outputDirectory).exists()){
newFile(outputDirectory).mkdirs();
}
//获取报告模板
File f =newFile("");
String absolutePath = f.getAbsolutePath();
String fileDir = absolutePath +"/template/";
String reslutpath = outputDirectory +"/html/report"+ReporterListener.formateDate()+".html";
File outfile =newFile(reslutpath);
if(!outfile.exists()){
outfile.mkdirs();
}
try{
//写文件
VelocityEngine ve =newVelocityEngine();
Properties p =newProperties();
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");
//输出结果
OutputStreamout=newFileOutputStream(newFile(reslutpath));
BufferedWriter writer =newBufferedWriter(newOutputStreamWriter(out,StandardCharsets.UTF_8));
// 转换输出
t.merge(context, writer);
writer.flush();
log.info("报告位置:"+ reslutpath);
}catch(IOException e){
e.printStackTrace();
}
}
/**
* 排序规则
*
* @param list
*/
privatevoid sort(List<ITestResult> list){
Collections.sort(list,newComparator<ITestResult>(){
@Override
publicint compare(ITestResult r1,ITestResult r2){
if(r1.getStatus()< r2.getStatus()){
return1;
}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.keytd> tr> <tr> <td>TOTAL: $item.totalSizetd> <td>SUCCESS: $item.successSizetd> <td>FAILED: $item.failedSizetd> <td>ERROR: $item.errorSizetd> <td>SKIPPED: $item.skippedSizetd> tr> <tr> <th>Statusth> <th>Methodth> <th>Descriptionth> <th>Durationth> <th>Detailth> 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.testNametd> <td>${testResult.description}td> <td>${testResult.duration} secondstd> <td class="detail"> ## <a class="button" href="#popup_log_${testResult.caseName}_${testResult.testName}">loga> <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>$msgpre> #end td> tr> #if($testResult.status==2) <tr> <th>异常th> <td> <pre>$testResult.throwableTracepre> td> tr> #end table> div> div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Closebutton> div> div> div> div> td> tr> #end table> #end
staticAndroidDriver<AndroidElement> driver;
/**
* 初始化运行类
*
* @param udid
* @param port
* @throws Exception
*/
@BeforeClass
@Parameters({"udid","port"})
publicvoidBeforeClass(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
publicvoid T001(){
LogUtil.info("启动");
driver.findElement(By.id("com.xunmeng.pinduoduo:id/bo0")).click();
}
使用xml启动
xml version="1.0" encoding="UTF-8"?> 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://www.cnblogs.com/yc-c/p/9015621.html 博客;有命令,就可以通过上面介绍的dos工具类启动服务端。
不足点:本次只是安卓端没有介绍H5怎么测试怎么定位,IOS怎么部署。