在编程中处理adb命令—App自动化测试与框架实战(10)
- 2019 年 12 月 12 日
- 筆記
顾翔老师开发的bugreport2script开源了,希望大家多提建议。文件在https://github.com/xianggu625/bug2testscript,
主文件是:zentao.py 。bugreport是禅道,script是python3+selenium 3,按照规则在禅道上书写的bugreport可由zentao.py程序生成py测试脚本。
来源:http://www.51testing.com
11.13 处理拖动
拖动就是将一个对象从一个位置拖到另外一个位置,可以简化桌面操作,如代码清单11-18所示。
代码清单11-18拖动
public void drag(By startElement_by, By endElement_by){TouchAction act = new TouchAction(driver) ;//定位元素的原位置MobileElement startElement = (MobileElement) driver.findElement(startElement_by);//定位元素要移动到的目标位置MobileElement endElement = (MobileElement) driver.findElement(endElement_by) ;//执行元素的移动操作act.press(startElement).perform();act.moveTo(endElement).release().perform();} |
---|
11.14 处理截图
Appium可以通过使用getScreenshotAs截取整个页面作为图片,在测试过程中帮助我们直观地定位错误,如代码清单11-19所示。
代码清单11-19截图操作
WebElement RegisterPage=driver.findElement(By.name("startUserRegistration"));FilescreenShot=driver.getScreenshotAs(OutputType.FILE);Filelocation=newFile("screenshots");String screenShotName=location.getAbsolutePath()+File.separator+"testCalculator.png";try{System.out.println("save screenshop");FileUtils.copyFile(screenShot,new File(screenShotName));}catch(IOException e){System.out.println("save screenshop fail");e.printStackTrace();}finally{System.out.println("save screenshop finish");} |
---|
受到设备存储容量的限制,我们可以考虑扩展这个功能,使得它可以截取页面上某一个元素。要截取页面上的username编辑框,代码如代码清单11-20所示。
代码清单11-20截取指定元素
WebElement username = driver.findElementById("inputUsername");username.sendKeys("bree");getElementShotSaveAs(username);Assert.assertEquals("liming", username.getText());public void getElementShotSaveAs(WebElement element) throws IOException{File screenShot=driver.getScreenshotAs(OutputType.FILE);BufferedImage img = ImageIO.read(screenShot);int width = element.getSize().width;int height = element.getSize().height;Rectangle rect = new Rectangle(width,height);Point p = element.getLocation();BufferedImage dest = img.getSubimage(p.x, p.y, rect.width, rect.height);ImageIO.write(dest, "png",screenShot);} |
---|
由于自动化测试是无人值守的,因此可以利用TestNG监听器来实现监听功能。当测试处于某种状态的时候执行错误截图,如测试失败时的截图。这里采用testListenerAdapter方法,每次测试失败的时候,都会重写该方法。
新建两个类,一个用作监听器,另外一个用于写测试代码。
1.监听器
监听器是一些预定义的Java接口。用户创建这些接口的实现类,并把它们加入TestNG中,TestNG 便会在测试运行的不同时刻调用这些类中的接口方法。这里使用ITestListener监听器,实现其方法为onTestFailure在测试失败的时候,保存控件的截图,如代码清单11-21所示。
代码清单11-21监听器
packageappiumsample;importjava.io.File;importjava.io.IOException;importio.appium.java_client.AppiumDriver;importorg.apache.commons.io.FileUtils;importorg.openqa.selenium.OutputType;importorg.testng.ITestResult;importorg.testng.TestListenerAdapter;publicclassScreenshotListenerextendsTestListenerAdapter{@OverridepublicvoidonTestFailure(ITestResulttr){AppiumDriverdriver=Screenshot.getDriver();Filelocation=newFile("screenshots");StringscreenShotName=location.getAbsolutePath()+File.separator+tr.getMethod().getMethodName()+".png";//使用ItestResult获取失败方法名FilescreenShot=driver.getScreenshotAs(OutputType.FILE);try{FileUtils.copyFile(screenShot,newFile(screenShotName));}catch(IOExceptione){e.printStackTrace();}}} |
---|
2.测试代码
通过使用"@Listeners"注释,可以直接在 Java 源代码中添加 TestNG 监听器,如代码清单11-22所示。
代码清单11-22测试代码
package appiumsample;import io.appium.java_client.android.AndroidDriver;import java.io.IOException;import java.net.MalformedURLException;import java.net.URL;import org.openqa.selenium.WebElement;import org.openqa.selenium.remote.DesiredCapabilities;import org.testng.Assert;import org.testng.annotations.AfterClass;import org.testng.annotations.BeforeClass;import org.testng.annotations.Listeners;import org.testng.annotations.Test;@Listeners({ScreenshotListener.class})public class Screenshot {private static AndroidDriver driver;@BeforeClasspublic void setup() throws MalformedURLException {//App地址String apppath = "F:\selendroid-test-app-0.15.0_debug.apk";//配置AndroidDriverDesiredCapabilities capabilities = new DesiredCapabilities();capabilities.setCapability("deviceName", "Lenovo A788t");//真机的名称为Lenovo A788tcapabilities.setCapability("platformVersion", "4.3");//操作系统版本为4.3capabilities.setCapability("platformName", "Android");//操作系统名称为Androidcapabilities.setCapability("udid", "00a10399");//使用的真机为Android平台capabilities.setCapability("app", apppath);//确定待测Appcapabilities.setCapability("appPackage", "io.selendroid.testapp");//待测App包capabilities.setCapability("appActivity", ".HomeScreenActivity");//待测App主Activity名capabilities.setCapability("automationName", "selendroid");driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"),capabilities);// WebDriverWait wait = new WebDriverWait(driver,10);}@SuppressWarnings("deprecation")@Testpublic void testExample() throws IOException {WebElement username = driver.findElementById("inputUsername");username.sendKeys("bree");Assert.assertEquals("liming", username.getText());}public static AndroidDriver getDriver(){return driver;}@AfterClasspublic void tearDown(){}} |
---|
11.15 隐式等待
在运行测试时,测试可能并不总是以相同的速度响应,例如,可能在几秒后进度条到100%时,按钮才会变成可单击的状态。这里介绍不同的方法进行同步测试。
隐式等待有两种方法,即implicitlyWait和sleep。需要注意的是,一旦设置了隐式等待,则它存在整个driver对象实例的生命周期中。在下例中,设置全局等待时间是30s,这是最长的等待时间。
最直接的方式是设置固定的等待时间。
Thread.sleep(30000)
对于固定等待时间的元素,可以用sleep进行简单的封装来实现等待指定的时间,如代码清单11-23所示。
代码清单11-23用sleep实现等待
@Test(description = "sleep简单封装")private boolean testisElementPresent(By by) throws InterruptedException {try {//设置等待时间Thread.sleep(1000);//查找元素driver.findElement(by);//若找到元素,返回truereturn true;} catch (NoSuchElementException e) {//若找不到元素,返回falsereturn false;}} |
---|
也可以利用sleep封装一个计时器,完成等待操作,如代码清单11-24所示。
代码清单11-24通过sleep计时器实现隐式等待
@Test(description = "sleep封装")public static void testwaitTimer( int units, int mills ) {DecimalFormat df = new DecimalFormat("###.##");double totalSeconds = ((double)units*mills)/1000;System.out.print("Explicit pause for " + df.format(totalSeconds) + " seconds dividedby " + units + " units of time: ");try {Thread.currentThread();int x = 0;while( x < units ) {Thread.sleep( mills );System.out.print("." );x = x + 1;}System.out.print('n');} catch ( InterruptedException ex ) {ex.printStackTrace();}} |
---|
隐式等待方式(implicitlyWait)是指在尝试发现某个元素的时候,如果没能立刻发现,就等待固定长度的时间。默认设置是0s,如代码清单11-25所示。
代码清单11-25implicitlyWait实现隐式等待
@Test(description = "测试显示等待")public void testImplicitlyWait(){//识别"美食"图标MobileElement meishiElement = (MobileElement) driver.findElement(By. xpath("//android.support.v7.widget.RecyclerView/android.widget.RelativeLayout[1]/android.widget.ImageView"));//单击"美食"图标,跳转到"美食"界面meishiElement.click();//设置全局等待时间最大为30sdriver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);//查找"深***"try{//查找"深***"driver.findElement(By.xpath("//android.widget.TextView[@text='深***']"));}catch(NoSuchElementException e){//如果控件没有找到,则测试用例执行失败Assert.fail("没有找到控件");}} |
---|
11.16 显示等待方法
在自动化测试的过程中,很多窗体内的数据,需要等待一会儿,才能加载完数据,才能出现一些元素,Driver才能操作这些元素。另外,做一些操作,本身可能也需要等待一会儿才有数据显示。
不管是否加载完成,隐式等待都会等待特定的时间,它会让一个正常响应的应用的测试变慢,增加了整个测试执行的时间。比如有些控件可能数据较多,需要较长时间才可以加载完成,但是其他控件加载很快,把它们都设置成固定等待时间,将会造成大量时间的浪费。因此,合理地设置时间等待是非常必要的。
Appium中提供了AppiumFluentWait来实现显示等待。AppiumFluentWait继承自FluentWait。这个类能支持一直等待知道特定的条件出现,使用AppiumFluentWait可以设置最大等待时间、等待的频率等,如代码清单11-26所示。
代码清单11-26显示等待
@Test(description = "测试FluentWait")public void testFluent(){//识别美团图标MobileElement meituan = (MobileElement) driver.findElement(By.xpath("// android.support.v7.widget.RecyclerView/android.widget.RelativeLayout[1]/android.widget.ImageView"));//创建AppiumFluentWait对象new AppiumFluentWait<MobileElement>(meituan)//最长等待时间为10s.withTimeout(10, TimeUnit.SECONDS)//每隔100ms判断一次元素的文本值是否为"深***".pollingEvery(100,TimeUnit.MILLISECONDS).until(new Function<MobileElement,Boolean>(){@Overridepublic Boolean apply(MobileElement element) {return element.getText().endsWith("深***");}});} |
---|
AppiumFluentWait的until的参数可以是Predicate,也可以是Function。这里使用的是Function。因为Function的返回值种类较多,可以为Object或者Boolean类型,而Predicate只能返回Boolean类型。
11.17 在编程中处理adb命令
在对App进行性能测试时,如获取CPU信息的命令为adb shell dumpsys cpuinfo packagename。在selendroid-test-app-0.15.0.apk实例中,要获取CPU的性能指标,编写的代码如代码清单11-27所示。
代码清单11-27获取CPU的性能指标
public static void GetCpu(String packageName) throws IOException {Runtime runtime = Runtime.getRuntime();Process proc = runtime.exec("adb shell dumpsys cpuinfo $"+packageName);try {if (proc.waitFor() != 0) {System.err.println("exit value = " + proc.exitValue());}BufferedReader in = new BufferedReader(new InputStreamReader(proc.getInputStream()));String line = null;String totalCpu = null;String userCpu = null;String kernalCpu = null;while ((line = in.readLine()) != null) {if(line.contains(packageName)){System.out.println(line);totalCpu = line.split("%")[0].trim();userCpu = line.substring(line.indexOf(":")+1,line.indexOf("% user")).trim();kernalCpu = line.substring(line.indexOf("+")+1, line.indexOf("% kernel")).trim();System.out.printf("totalCpu的值为:%s%n", totalCpu);System.out.printf("userCpu的值为:%s%n", userCpu);System.out.printf("kernalCpu的值为:%s%n", kernalCpu);}}} catch (InterruptedException e) {System.err.println(e);}finally{try {proc.destroy();} catch (Exception e2) {}}} |
---|
输出结果如图11-7所示。

图11-7 CPU性能指标
在实际的测试过程中可以多次调用上述代码,以获取不同阶段的CPU值。其他性能指标的获取方法类似。
11.18 区分WebElement、MobileElement、AndroidElement和iOSElement
在Appium自动化测试中,可能有些初学者会对获取控件元素对象的类型存在疑惑,不知道在什么情况下使用什么类型。下面将介绍控件元素对象类型的区别。
" WebElement可以使用所有的Selenium命令。
" MobileElement属于Appium,继承自WebElement,但是又增加了一些Appium特有的功能(如Touch手势)。
" AndroidElement和iOSElement实现了WebElement接口方法,并增加了一些Android和iOS特有的功能(如findByAndroidUiAutomation)。
根据待测手机操作系统平台,可以选择不同的应用,或者根据是否跨平台进行选择。
11.19 区分RemoteWebDriver、AppiumDriver、AndroidDriver和iOSDriver
在Appium自动化测试中,可能有些初学者会对创建什么类型的驱动产生困惑,本节将介绍各个驱动类型的区别。
" RemoteWebDriver:这个驱动来自于Selenium,可以使执行测试的机器和发送测试命令的机器独立开来,中间存在网络请求。Appium是基于客户端/服务器的,所有RemoteWebDriver可以直接初始化会话。但是一般不建议使用,Appium提供了其他驱动,可能在使用上更加方便。
" AppiumDriver:继承自RemoteWebDriver,但是增加了一些特有的功能(如上下文切换)。
" AndroidDriver:继承自AppiumDriver,但是增加了一些特有的功能,如openNtificutions方法,只有在Android设备或者Android模拟器上才使用这个驱动。
" iOSDriver:继承自AppiumDriver,但是增加了一些特有的功能,只有在iOS设备或者iOS模拟器上才使用这个驱动。
在实际的使用场景中,根据手机操作系统不同,建议直接使用AndroidDriver或者iOSDriver。
11.20 在代码中启动服务器
在Appium测试执行时,需要手动启动Appium服务器。在一些并行测试场景下,要启动多个Appium服务器,如果在代码中未使用driver.quit关闭服务器,或者存在其他一些异常,就会出现会话无法创建的情况。Appium官网提供了AppiumDriverLocalService来完成Appium服务器的启动和关闭。这一节讲述如何设置Appium服务器的启动和关闭,可以根据项目要求进行集成。
使用AppiumDriverLocalService的前提条件有以下两个。
" 安装Node.js 7以上版本。
" 通过npm安装Appium服务器。
具体的操作如下。
(1)如果没有指定参数,实现方式如代码清单11-28所示。
代码清单11-28 未指定参数
import io.appium.java_client.service.local.AppiumDriverLocalService;…AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService();service.start();…service.stop(); |
---|
本地环境中可能会在这一步报错。
AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService();
这个问题在UNIX/Linux下面比较常见,可能是因为使用的node.js实例与环境变量设置的实例不是同一个,也有可能是Appium node服务导致的(Appium.js版本小于等于1.4.16,Main.js版本大于等于1.5.0)。在这种情况下,建议用户设置NODE_BINARY_PATH(Windows操作系统下指node.exe所在路径,Linux/Mac OS下指node所在路径)和APPIUM_BINARY_PATH(Appium.js和Main.js的执行路径)到环境变量中,也可以在程序中指定。
//appium.node.js.exec.pathSystem.setProperty(AppiumServiceBuilder.NODE_PATH ,"the path to the desired node.js executable");System.setProperty(AppiumServiceBuilder.APPIUM_PATH ,"the path to the desired appium.js or main.js");AppiumDriverLocalService service = AppiumDriverLocalService.buildDefaultService(); |
---|
(2)指定参数,如代码清单11-29所示。
代码清单11-29 指定参数
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;…AppiumDriverLocalService service = AppiumDriverLocalService.buildService(new AppiumServiceBuilder().withArgument(GeneralServerFlag.TEMP_DIRECTORY,"The_path_to_the_temporary_directory"));或者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;…AppiumDriverLocalService service = new AppiumServiceBuilder().withArgument(GeneralServerFlag.TEMP_DIRECTORY,"The_path_to_the_temporary_directory").build(); |
---|
需要导入以下3个包。
io.appium.java_client.service.local.flags.GeneralServerFlagio.appium.java_client.service.local.flags.AndroidServerFlagio.appium.java_client.service.local.flags.iOSServerFlag |
---|
(3)定义参数。
在有些情况下可能需要使用一些特殊的端口(指定端口)。
new AppiumServiceBuilder().usingPort(4000);
或者使用那些未使用的端口。
new AppiumServiceBuilder().usingAnyFreePort();
使用其他的IP地址。
new AppiumServiceBuilder().withIPAddress("127.0.0.1");
确定日志文件。
import java.io.File;…new AppiumServiceBuilder().withLogFile(logFile); |
---|
Node.js执行路径,如代码清单11-30所示。
代码清单11-30 Node.js执行路径
import java.io.File;…new AppiumServiceBuilder().usingDriverExecutable(nodeJSExecutable); |
---|
Main.js执行路径,如代码清单11-31所示。
代码清单11-31 Main.js执行路径
import java.io.File;…//appium.js is the full or relative path to//the appium.js (v<=1.4.16) or maim.js (v>=1.5.0)new AppiumServiceBuilder().withAppiumJS(new File(appiumJS)); |
---|
确定服务器端的Desired Capabilities,如代码清单11-32所示。
代码清单11-32 确定服务器端的Desired Capabilities
DesiredCapabilities serverCapabilities = new DesiredCapabilities();…//the capability fillingAppiumServiceBuilder builder = new AppiumServiceBuilder().withCapabilities(serverCapabilities);AppiumDriverLocalService service = builder.build();service.start();…service.stop(); |
---|
11.21 PageFactory注解
第8章中使用了Page Object和PageFactory两种设计模式。这一节将详细阐述Appium官方关于Page Object和PageFactory的使用,并通过实例加深对它们的认识,以便在实际使用中对这些概念不会产生疑惑并能灵活地根据需求进行设置。更复杂的使用场景参考官方文档。
(1)如代码清单11-33所示,默认设置为WebElement或WebElement 数组,注释方式使用FindBy,元素类型为WebElement。
代码清单11-33 FindBy实例
import org.openqa.selenium.WebElement;import org.openqa.selenium.support.FindBy;…@FindBy(someStrategy) //for browser or web view html UI//also for mobile native applications when other locator strategies are not definedWebElement someElement;@FindBy(someStrategy) //for browser or web view html UI//also for mobile native applications when other locator strategies are not definedList<WebElement> someElements; |
---|
(2)指定具体的定位策略。如代码清单11-34所示,根据Desired Capability中设置的automationName自动化测试引擎的值,针对移动原生应用(Native App),分别使用"@ AndroidFindBy""@ SelendroidFindBy"和"@ iOSFindBy"进行注解,元素类型为AndroidElement、RemoteWebElement以及IOSElement。
代码清单11-34 指定具体的定位策略
import io.appium.java_client.android.AndroidElement;import org.openqa.selenium.remote.RemoteWebElement;import io.appium.java_client.pagefactory.*;import io.appium.java_client.ios.IOSElement;@AndroidFindBy(someStrategy) //for Android UI when Android UI automator is usedAndroidElement someElement;@AndroidFindBy(someStrategy) //for Android UI when Android UI automator is usedList<AndroidElement> someElements;@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is usedRemoteWebElement someElement;@SelendroidFindBy(someStrategy) //for Android UI when Selendroid automation is usedList<RemoteWebElement> someElements;@iOSFindBy(someStrategy) //for iOS native UIIOSElement someElement;@iOSFindBy(someStrategy) //for iOS native UIList<IOSElement> someElements; |
---|
(3)跨平台的原生App测试实例,如代码清单11-35所示。针对原生App,使用"@AndroidFindBy"和"@iOSFindBy"同时进行注解。元素的类型为MobileElement。
代码清单11-35 跨平台的原生App测试实例
import io.appium.java_client.MobileElement;import io.appium.java_client.pagefactory.*;@AndroidFindBy(someStrategy)@iOSFindBy(someStrategy)MobileElement someElement;@AndroidFindBy(someStrategy) //for the crossplatform mobile native@iOSFindBy(someStrategy) //testingList<MobileElement> someElements; |
---|
(4)全平台的测试实例,如代码清单11-36所示。其中使用"@FindBy""@AndroidFindBy"以及"@iOSFindBy"同时进行注解。元素的类型为RemoteWebElement。
代码清单11-36 全平台的测试实例
import org.openqa.selenium.remote.RemoteWebElement;import io.appium.java_client.pagefactory.*;import org.openqa.selenium.support.FindBy;//the fully cross platform examle@FindBy(someStrategy) //for browser or web view html UI@AndroidFindBy(someStrategy) //for Android native UI@iOSFindBy(someStrategy) //for iOS native UIRemoteWebElement someElement;//the fully cross platform examle@FindBy(someStrategy)@AndroidFindBy(someStrategy) //for Android native UI@iOSFindBy(someStrategy) //for iOS native UIList<RemoteWebElement> someElements; |
---|
(5)用Chained或者Possible定位方式。
" Chained定位方式。使用"@FindBys""@AndroidFindBys"和"@iOSFindBy"进行注解。元素内容通过多种定位方法找到。FindBys相当于在多种定位方式中取交集,如"@FindBys({@FindBy(someStrategy1)""@FindBy(someStrategy2)})"相当于首先根据someStrategy1找到对应元素,然后在这些元素中通过someStrategy2再次查找元素,这类似于driver.findelement(someStrategy1). findelement(someStrategy2),如代码清单11-37和代码清单11-38所示。
代码清单11-37 Chained定位方式之一
import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; import org.openqa.selenium.support.FindBys; import org.openqa.selenium.support.FindBy; @FindBys({@FindBy(someStrategy1), @FindBy(someStrategy2)}) @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2) RemoteWebElement someElement; @FindBys({@FindBy(someStrategy1), @FindBy(someStrategy2)}) @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2) List<RemoteWebElement> someElements;
代码清单11-38 Chained定位方式之二
importorg.openqa.selenium.remote.RemoteWebElement; importio.appium.java_client.pagefactory.*; importorg.openqa.selenium.support.FindBys; importorg.openqa.selenium.support.FindBy; importstaticio.appium.java_client.pagefactory.LocatorGroupStrategy.CHAIN; @HowToUseLocators(androidAutomation=CHAIN,iOSAutomation=CHAIN) @FindBys({@FindBy(someStrategy1),@FindBy(someStrategy2)}) @AndroidFindBy(someStrategy1)@AndroidFindBy(someStrategy2) @iOSFindBy(someStrategy1)@iOSFindBy(someStrategy2) RemoteWebElementsomeElement; @HowToUseLocators(androidAutomation=CHAIN,iOSAutomation=CHAIN) @FindBys({@FindBy(someStrategy1),@FindBy(someStrategy2)}) @AndroidFindBy(someStrategy1)@AndroidFindBy(someStrategy2) @iOSFindBy(someStrategy1)@iOSFindBy(someStrategy2) List<RemoteWebElement>someElements;
" Possible定位方式。如代码清单11-39所示,这种定位方式指使用"@FindAll""@AndroidFindAll"和"@iOSFindAll"进行注解。FindAll相当于在多种定位方式中取并集,如"@FindAll{@FindBy(someStrategy1)","@FindBy(someStrategy2)})"相当于取到所有符合someStrategy1和someStrategy2的元素。
代码清单11-39 Possible定位方式
import org.openqa.selenium.remote.RemoteWebElement; import io.appium.java_client.pagefactory.*; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.FindByAll; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; @HowToUseLocators(androidAutomation = ALL_POSSIBLE, iOSAutomation = ALL_POSSIBLE) @FindAll{@FindBy(someStrategy1), @FindBy(someStrategy2)}) @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2) RemoteWebElement someElement; @HowToUseLocators(androidAutomation = ALL_POSSIBLE, iOSAutomation = ALL_POSSIBLE) @FindAll({@FindBy(someStrategy1), @FindBy(someStrategy2)}) @AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) @iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2) List<RemoteWebElement> someElements;
星云测试
http://www.teststars.cc
奇林软件
http://www.kylinpet.com
联合通测
http://www.quicktesting.net