Naga——基于 WebDriverAgent 的 iOS 自动化测试实现

Naga是什么?

Naga
NagaTest

Naga是个代号,它是基于java语言的iOS自动化测试实现,支持iOS 9.x+版本的Native,Hybrid,H5的UI自动化测试。
Naga非常轻盈,它通过jar的方式分发,在你的测试工程中引用naga.jar即可开始您的iOS自动化测试之旅。
Naga环境部署简单快捷,较少的依赖能让您快速的完成环境搭建。

为什么会有Naga?

这个想法开始于今年年初,刚换了新工作总想用工作之余和闲暇时间来做点什么,因为近几年的工作都与移动UI自动化测试紧密相关,前后用过Appium,macaca,还试了一下ATX。目前的工作中自动化测试的开展都是以Appium为主,也使用过一段时间的macaca。由于大多经验都与之相关,所以谋发出了编写一个轻量级移动自动化测试解决方案的想法。Naga的一些想法和解决方案均来源于这两个优秀的开源框架。

Naga的工作原理

Naga跟Appium和macaca一样都使用了WebDriverAgent来驱动Native部分的iOS自动化测试,使用ios_webkit_debug_proxy配合selenium-atoms来驱动Web部分。
Naga工作原理简单示意图:

Naga环境搭建

  • Mac OS 10.11.5+(推荐10.12)

  • Xcode 8.2+(在8.2.1上测试OK,未更新8.3)

  • Jdk 1.7+(推荐JDK1.8)

  • Brew

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  • ios-webkit-debug-proxyy
brew install ios-webkit-debug-proxy
  • usbmuxd(真机运行必须安装)
brew install usbmuxd
  • WebDriverAgent

Naga上的WebDriverAgent.zip解压并复制到/usr/local/lib/node_modules/目录下即可。如图所示:

Naga API

  • NagaDriver类
void acceptAlert() 
accept alert

String alertText()
get the alert text

void context(String context)
set the context by context name when set WEBVIEW context. you can use the page id or the page tile, such as WEBVIEW_1 or WEBVIEW_title

void dismissAlert()
dismiss alert

Element findElementByClassName(String className)
find Element by class name

Element findElementByCssSelector(String css)
find Element by css selector

Element findElementById(String id)
find Element by id

Element findElementByLinkText(String linkText)
find Element by link text

Element findElementByName(String name)
find Element by name

Element findElementByPartialLinkText(String partialLinkText)
find Element by partial link text

Element findElementByTagName(String tagName)
find Element by tag name

Element findElementByXPath(String xpath)
find Element by xpath

Elements findElementsByClassName(String className)
find Elements by class name

Elements findElementsByCssSelector(String css)
find Elements by css selector

Elements findElementsById(String id)
find Elements by id

Elements findElementsByLinkText(String linkText)
find Elements by link text

Elements findElementsByName(String name)
find Elements by name

Elements findElementsByPartialLinkText(String partialLinkText)
find Elements by partial link text

Elements findElementsByTagName(String tagName)
find Elements by tag name

Elements findElementsByXPath(String xpath)
find Elements by xpath

java.util.List<String> getContexts()
get contexts

String getCurrentContext()
get current context name

void hideKeyboard()
hide the keyboard. The keyboard on iPhone cannot be dismissed because of a known XCTest issue.

void home()
click home

NagaDriver initDriver(JSONObject capabilities)
init NagaDriver

void quit()
quit driver

void saveScreenShot(String fileName)
save the screenshot to a file

String screenShot()
get the screenshot base64 code

String sessionId()
get the current session id

void sleep(long sec)
String source()
get page source

Integer status()
get session status

void swipe(double startx, double starty, double endx, double endy, double duruation)
drag the screen from start location to end location

void tap(double x, double y)
tap the screen by location

void toUrl(String url)
go to the website by url WEBVIEW ONLY

JSONObject window_size()
get the screen size


  • Element类
void clear() 
clear the Element text

void click()
click the Element

Element findElementByClassName(String className)
find the child element NATIVE ONLY

Element findElementByCssSelector(String cssSelector)
find the child element NATIVE ONLY

Element findElementById(String id)
find the child element NATIVE ONLY

Element findElementByLinkText(String linkText)
find the child element NATIVE ONLY

Element findElementByName(String name)
find the child element NATIVE ONLY

Element findElementByPartialLinkText(String partialLinkText)
find the child element NATIVE ONLY

Element findElementByTagName(String tagName)
find the child element NATIVE ONLY

Element findElementByXPath(String xpath)
find the child element NATIVE ONLY

Elements findElementsByClassName(String className)
find the child elements NATIVE ONLY

Elements findElementsByCssSelector(String cssSelector)
find the child elements NATIVE ONLY

Elements findElementsById(String id)
find the child elements NATIVE ONLY

Elements findElementsByLinkText(String linkText)
find the child elements NATIVE ONLY

Elements findElementsByName(String name)
find the child elements NATIVE ONLY

Elements findElementsByPartialLinkText(String partialLinkText)
find the child elements NATIVE ONLY

Elements findElementsByTagName(String tagName)
find the child elements NATIVE ONLY

Elements findElementsByXPath(String xpath)
find the child elements NATIVE ONLY

String getAttribute(String name)
get the attribute from the Element by attribute name

String getComputedCss(String name)
get computed css from Element WEBVIEW only

JSONObject getRect()
get rect by Element

String getText()
get the Element text

boolean isDisplayed()
get the Element is display or not

void sendKeys(String text)
send text to the Element
  • Elements类
Element get(int index) 
get the Element for the Elements by index

int size()
get size of this Elements

在真机上测试

  • WebDriverAgent签名

首先需要对WebDriverAgent进行签名(普通AppleId即可,推荐使用开发者账号签名避免个人账号签名过期的问题),需要对WebDriverAgentLib和WebDriverAgentRunner都进行签名,签名完成后Build校验签名是否正确。
推荐使用Xcode–Product–Test在真机上安装上WebDriverAgentRunner。第一次安装时如使用个人账号签名需要在设置–通用–描述文件与设备管理中信任你签名的账号,再次安装方可成功。

  • 被测应用

被测试的app可以是从appstore下载的app(需要知道该app的bundleId),也可以是签名过的app(签名步骤同WebDriverAgent签名),如需要安装app,可在initDriver时传入app参数,如已安装app可使用bundleId参数。(暂未实现是否卸载再安装的方法,均使用WebDriverAgent原生方案)

NagaTest

使用xdf提供的ios-app-bootstrap项目做测试示例,如需要在真机测试请下载该工程对其进行签名后使用。
更多测试示例请前往NagaTest

NagaDriver driver = new NagaDriver();

@Before
public void setUp() throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("bundleId", "xdf.ios-app-bootstrap");
jsonObject.put("app", System.getProperty("user.dir") + "/apps/ios-app-bootstrap.app");
JSONObject desiredCapabilities = new JSONObject();
desiredCapabilities.put("port", "8901");
desiredCapabilities.put("udid", "9CC1ADCC-94CE-47C3-AA4F-5008AE463119");
desiredCapabilities.put("desiredCapabilities", jsonObject);
driver.initDriver(desiredCapabilities);
}

@Test
public void test_hybrid() throws Exception {
// set the screenshot image where to save
String imageFilepath = System.getProperty("user.dir");

System.out.println("------------#1 login test-------------------");

driver.findElementByClassName("XCUIElementTypeTextField").sendKeys("中文+Test+12345678");
driver.findElementByXPath("//XCUIElementTypeSecureTextField[1]").sendKeys("111111");
driver.findElementByName("Done").click();
driver.findElementByName("Login").click();
driver.sleep(1);

System.out.println("------------#2 scroll tableview test-------------------");

driver.findElementByName("HOME").click();
driver.findElementByName("list").click();
driver.sleep(1);
driver.swipe(200, 420, 200, 120, 1);
driver.sleep(2);

System.out.println("------------#3 webview test-------------------");

driver.findElementByName("Webview").click();
driver.sleep(3);
// save screen shot
driver.saveScreenShot(imageFilepath + "/webView.png");

driver.context("WEBVIEW_test");
driver.sleep(2);
driver.findElementById("pushView").click();
driver.sleep(3);
driver.findElementById("popView").click();
driver.sleep(2);

System.out.println("------------#4 baidu web test-------------------");
switchToNative();
driver.findElementByName("Baidu").click();
driver.sleep(5);
driver.saveScreenShot(imageFilepath + "/baidu.png");

driver.context("WEBVIEW_百度一下");
// driver.context("WEBVIEW_2");
driver.findElementById("index-kw").sendKeys("中文+TesterHome");
driver.findElementById("index-bn").click();
driver.sleep(5);
System.out.println(driver.source());

System.out.println("------------#5 logout test-------------------");

switchToNative();
driver.findElementByName("PERSONAL").click();
driver.sleep(1);
driver.findElementByName("Logout").click();
driver.sleep(1);
}

// switch to native
public void switchToNative() throws Exception {
driver.context("NATIVE_APP");
}

@After
public void tearDown() throws Exception {
driver.quit();
}

Naga征集试用用户

Naga还是位嗷嗷待哺的婴儿,需要各位有兴趣的朋友加入试用并反馈试用感受,我会非常认真的处理每一位的试用结果和感受(对它的成长帮助非常大的我还会给出一定的红包奖励)。
在此征集第一批试用人员,要求如下:
1.您需要一颗爱心(它太稚嫩了,可能有很多不足甚至奇奇怪怪的bug);
2.您需要一台有Mac OS X 10.11或Mac OS 10.12系统的电脑(虚拟机亦可);
3.您需要有一定的java编程基础;
有兴趣的朋友欢迎您加入

Naga的未来

这是我的第一个个人项目,我一定会努力将它做好。
Naga未来会考虑加入对Android的支持,先做好iOS部分。
Naga暂时不会公开源码,分发的naga.jar也经过了一点简单的混淆(不过还原难度不大,有兴趣的朋友可以反编译看看)。

2017.04.13更新

支持Xcode8.3.1和iOS10.3设备的测试支持;
更新了WebDriverAgent,请下载WebDriverAgent-iOS10.3.zip