[iOS] UI自动化测试

什么是UI自动化测试呢?就是说我们跑一个程序,然后就会看到app跑起来,并且不用我们做操作,它自己会实现各种点击跳转之类的,这样我们就可以把一些标准case转化为代码,每次发包之前跑一次,减少了QA的操作。


1. XCode 7之前的 UI Automation

参考:https://www.jianshu.com/p/0e28ae1bd2c2

这个part不会仔细说,因为现在的Xcode已经没有这个功能了,如果感兴趣可以看上面的文章,大概的操作就是通过instruments里面的Automation,然后输入脚本:

编写脚本的位置

2. XCTest框架

Xcode 8 及以后,苹果将之前的自动化Automation换成了内置的代码target,可以运行起来进行自动化的展示点击之类的。

可以参考:https://www.jianshu.com/p/10795157fdc0

其实挺简单的,主要是以下几个步骤:

  • create UI Testing target


    创建target
  • code cases


    写代码
  • run the target (点那个小三角运行)


    run test
如何写UI测试代码

不管是Unit Test Class还是 UI Test Class 都继承自XCTestCase。Test Class中编写的测试方法都要以test开头,否则的话不会执行。因此,如果想暂时关闭测试方法,可以再方法名前面加一个Disable前缀,简洁明了,func DISABLE_testAddition() 这样。下图是一个Test Method 实现:

- (void)testButtonClick {
    XCUIApplication *app = [[XCUIApplication alloc] init];
    [app launch];
    [app.buttons[@"ScrollTestViewController"] tap];
    NSLog(@"ScrollTestViewController tapped");
    
    sleep(10);
}

如果我们打开Test Class 会看到每个测试类都回重写setUp()tearDown()方法。
其实所有的test方法是异步执行的,并且在执行之前都会调用setUp()方法,在执行之后都会调用tearDown()方法

因此有些重复的公有代码,可以放在setUp()tearDown()方法中, 比如启动程序,设置公有的测试模拟数据等。在测试的执行代码运行后,通常会使用XCAssert来断言运行结果XCAssert会很明显的呈现测试的结果,一旦设置的断言条件不通过,就会中断并显示结果,有助于我们的定位问题。


XCTest框架briefing

这一部分只是一些简要的东西,具体内容还是要看官方文档。XCTest Framework是 UITest 实现的关键,主要由XCUIApplicationXCUIElementXCUIElementQuery三个类以及XCUIElementAttributesXCUIElementTypeQueryProvider两个协议构成。

XCUIApplication类继承自XCUIElement类,XCUIElement类遵循XCUIElementAttributesXCUIElementTypeQueryProvider协议,而XCUIElementTypeQueryProvider协议返回的UI元素对象则是XCUIElementQuery类。

XCUIApplication实现application的launch、 terminal、 active、 state等功能, XCUIElement定义元素的操作事件,XCUIElementTypeQueryProvider协议实现查询所有的UI元素对象,XCUIElementAttributes则表示元素的属性,XCUIElementQuery表示元素的一些功能等等。

来看一个UI Test的实现,通过XCUIElementTypeQueryProvider获取到UI 元素对象,并且向对象发送事件。

class CalcUITests: XCTestCase {

let app = XCUIApplication() //获取XCUIApplication对象
     
override func setUp() {
    super.setUp()
    
    continueAfterFailure = false
    XCUIApplication().launch() //XCUIApplication加载
}

override func tearDown() {
    super.tearDown()
}

func testAddition() {
    
     //查询到app的元素,并发送事件
    app.buttons["6"].tap()
    app.buttons["+"].tap()
    app.buttons["2"].tap()
    app.buttons["="].tap()
    
    if let textFieldValue = app.textFields["display"].value as? String {
        XCTAssertTrue(textFieldValue == "8", "Part 1 failed.")
    }
}

我们能从element上面获取哪些信息呢,其实就是XCUIElementAttributes的内容:

@protocol XCUIElementAttributes

/*! The accessibility identifier. */
@property (readonly) NSString *identifier;

/*! The frame of the element in the screen coordinate space. */
@property (readonly) CGRect frame;

/*! The raw value attribute of the element. Depending on the element, the actual type can vary. */
@property (readonly, nullable) id value;

/*! The title attribute of the element. */
@property (readonly, copy) NSString *title;

/*! The label attribute of the element. */
@property (readonly, copy) NSString *label;

/*! The type of the element. /seealso XCUIElementType. */
@property (readonly) XCUIElementType elementType;

/*! Whether or not the element is enabled for user interaction. */
@property (readonly, getter = isEnabled) BOOL enabled;

/*! The horizontal size class of the element. */
@property (readonly) XCUIUserInterfaceSizeClass horizontalSizeClass;

/*! The vertical size class of the element. */
@property (readonly) XCUIUserInterfaceSizeClass verticalSizeClass;

/*! The value that is displayed when the element has no value. */
@property (readonly, nullable) NSString *placeholderValue;

/*! Whether or not the element is selected. */
@property (readonly, getter = isSelected) BOOL selected;

#if TARGET_OS_TV
/*! Whether or not the elment has UI focus. */
@property (readonly) BOOL hasFocus;
#endif

@end

我们能从app里面拿到哪些东西呢,其实就是XCUIElementTypeQueryProvider里面的内容,例如buttons:

@protocol XCUIElementTypeQueryProvider

@property (readonly, copy) XCUIElementQuery *touchBars;
@property (readonly, copy) XCUIElementQuery *groups;
@property (readonly, copy) XCUIElementQuery *windows;
@property (readonly, copy) XCUIElementQuery *sheets;
@property (readonly, copy) XCUIElementQuery *drawers;
@property (readonly, copy) XCUIElementQuery *alerts;
@property (readonly, copy) XCUIElementQuery *dialogs;
@property (readonly, copy) XCUIElementQuery *buttons;
@property (readonly, copy) XCUIElementQuery *radioButtons;
@property (readonly, copy) XCUIElementQuery *radioGroups;
@property (readonly, copy) XCUIElementQuery *checkBoxes;
@property (readonly, copy) XCUIElementQuery *disclosureTriangles;
@property (readonly, copy) XCUIElementQuery *popUpButtons;
@property (readonly, copy) XCUIElementQuery *comboBoxes;
@property (readonly, copy) XCUIElementQuery *menuButtons;
@property (readonly, copy) XCUIElementQuery *toolbarButtons;
@property (readonly, copy) XCUIElementQuery *popovers;
@property (readonly, copy) XCUIElementQuery *keyboards;
@property (readonly, copy) XCUIElementQuery *keys;
@property (readonly, copy) XCUIElementQuery *navigationBars;
@property (readonly, copy) XCUIElementQuery *tabBars;
@property (readonly, copy) XCUIElementQuery *tabGroups;
@property (readonly, copy) XCUIElementQuery *toolbars;
@property (readonly, copy) XCUIElementQuery *statusBars;
@property (readonly, copy) XCUIElementQuery *tables;
@property (readonly, copy) XCUIElementQuery *tableRows;
@property (readonly, copy) XCUIElementQuery *tableColumns;
@property (readonly, copy) XCUIElementQuery *outlines;
@property (readonly, copy) XCUIElementQuery *outlineRows;
@property (readonly, copy) XCUIElementQuery *disclosedChildRows;
@property (readonly, copy) XCUIElementQuery *browsers;
@property (readonly, copy) XCUIElementQuery *collectionViews;
@property (readonly, copy) XCUIElementQuery *sliders;
@property (readonly, copy) XCUIElementQuery *pageIndicators;
@property (readonly, copy) XCUIElementQuery *progressIndicators;
@property (readonly, copy) XCUIElementQuery *activityIndicators;
@property (readonly, copy) XCUIElementQuery *segmentedControls;
@property (readonly, copy) XCUIElementQuery *pickers;
@property (readonly, copy) XCUIElementQuery *pickerWheels;
@property (readonly, copy) XCUIElementQuery *switches;
@property (readonly, copy) XCUIElementQuery *toggles;
@property (readonly, copy) XCUIElementQuery *links;
@property (readonly, copy) XCUIElementQuery *images;
@property (readonly, copy) XCUIElementQuery *icons;
@property (readonly, copy) XCUIElementQuery *searchFields;
@property (readonly, copy) XCUIElementQuery *scrollViews;
@property (readonly, copy) XCUIElementQuery *scrollBars;
@property (readonly, copy) XCUIElementQuery *staticTexts;
@property (readonly, copy) XCUIElementQuery *textFields;
@property (readonly, copy) XCUIElementQuery *secureTextFields;
@property (readonly, copy) XCUIElementQuery *datePickers;
@property (readonly, copy) XCUIElementQuery *textViews;
@property (readonly, copy) XCUIElementQuery *menus;
@property (readonly, copy) XCUIElementQuery *menuItems;
@property (readonly, copy) XCUIElementQuery *menuBars;
@property (readonly, copy) XCUIElementQuery *menuBarItems;
@property (readonly, copy) XCUIElementQuery *maps;
@property (readonly, copy) XCUIElementQuery *webViews;
@property (readonly, copy) XCUIElementQuery *steppers;
@property (readonly, copy) XCUIElementQuery *incrementArrows;
@property (readonly, copy) XCUIElementQuery *decrementArrows;
@property (readonly, copy) XCUIElementQuery *tabs;
@property (readonly, copy) XCUIElementQuery *timelines;
@property (readonly, copy) XCUIElementQuery *ratingIndicators;
@property (readonly, copy) XCUIElementQuery *valueIndicators;
@property (readonly, copy) XCUIElementQuery *splitGroups;
@property (readonly, copy) XCUIElementQuery *splitters;
@property (readonly, copy) XCUIElementQuery *relevanceIndicators;
@property (readonly, copy) XCUIElementQuery *colorWells;
@property (readonly, copy) XCUIElementQuery *helpTags;
@property (readonly, copy) XCUIElementQuery *mattes;
@property (readonly, copy) XCUIElementQuery *dockItems;
@property (readonly, copy) XCUIElementQuery *rulers;
@property (readonly, copy) XCUIElementQuery *rulerMarkers;
@property (readonly, copy) XCUIElementQuery *grids;
@property (readonly, copy) XCUIElementQuery *levelIndicators;
@property (readonly, copy) XCUIElementQuery *cells;
@property (readonly, copy) XCUIElementQuery *layoutAreas;
@property (readonly, copy) XCUIElementQuery *layoutItems;
@property (readonly, copy) XCUIElementQuery *handles;
@property (readonly, copy) XCUIElementQuery *otherElements;
@property (readonly, copy) XCUIElementQuery *statusItems;

/*!
 * Returns an element that will use the query for resolution. This changes how the query is resolved
 * at runtime; instead of evaluating against every element in the user interface, `firstMatch` stops
 * the search as soon as a single matching element is found. This can result in significantly faster
 * evaluation if the element is located early in a large view hierarchy but also means that multiple
 * matches will not be detected.
 */
@property (readonly) XCUIElement *firstMatch;

@end

我们可以对element做的操作可以看XCUIElement,因为比较多所以就不都贴出来啦,看几个叭:

/*!
 * Moves the cursor over the element.
 */
- (void)hover;

/*!
 * Sends a click event to a hittable point computed for the element.
 */
- (void)click;

/*!
 * Sends a double click event to a hittable point computed for the element.
 */
- (void)doubleClick;

/*!
 * Sends a right click event to a hittable point computed for the element.
 */
- (void)rightClick;

单元测试和 UI 测试的区别

对于Unit Test 和 UI Test,两者实现的方式是截然不同的。单元测试的实现方式是保证测试目标类的访问权限,实例化这个类 或者 模拟这个类 (使用类似 OCMock的方式 ),然后在测试方法中调用这个类需测试的方法、功能。

而对 UI Test,则依靠XCFramework实现。利用XCUIApplication、 XCUIElement 、XCUIElementQuery来获取的App的各个UI对象,同步事件等等,并且将触发事件发送给UI对象。

Unit Test 会访问目标类的内部方法,而UI Test则不会,只会在外部触发事件。


集成

我们编写测试代码的最终目的其实是为了方便测试和修改,这样的话测试自动化即持续集成会带来很多益处。即时发现代码问题、保证主干版本质量、节约测试时间成本等等。因此将Test持续集成到CI工具是很好的做法。Xcode可以很好的和OS X Server结合使用,关联到 Git 版本库之后创建包含 Test 的bot,持续构建 测试 和 部署。

当然了你还可以使用xcodebuild命令行工具编写自动构建、测试脚本,结合Fastlane 和 Jenkins ,实现自动化测试。


3. 远程控制测试(电脑控制手机自动跑case但不用xcode)

这一块主要是通过手机端的Python代码远程遥控手机的自动测试,主要是借用wda框架。

WebDriverAgent briefing

可以参考:https://www.jianshu.com/p/b2007f520c77

首先是这个框架是干啥的?WDA(WebDriverAgent)是Facebook推出的一个开源ios自动化测试框架。(官方链接:https://github.com/facebookarchive/WebDriverAgent)

你可以通过mac的 xcode WebDriverAgent 工程 run起一个wda的程序在手机上,然后手机就成为了一个server,电脑就可以通过端口控制手机跑自动测试(自动测试仍旧是借助于XCTest的框架哒),server也就是手机再通过调用XCTest.framework和调用苹果的API直接在设备上执行命令。

wda原理

WebDriverAgent setup

这部分会分为两个part,在手机上run起wda的工程 & 编写Python程序来调动手机的自动测试

(1)在手机上run起wda的工程
  • 安装各种wda依赖的库
① brew install carthage

② brew install node

③ brew install --HEAD libimobiledevice

④ npm install -g iproxy
  • 下载wda源码
git clone https://github.com/appium/WebDriverAgent
  • 初始化
    进入wda源码下载的路径,执行 ./Scripts/bootstrap.sh。

  • 改签名(这里需要用自己的开发者账号)


    改sign
改runner的bundle id
  • 通过 Product->Test 跑到手机上面


    跑到手机上面
  • 如果成功的话你会在xcode的console里面看到这些:

2020-09-07 14:01:50.959728+0800 WebDriverAgentRunner-Runner[1387:217902] Built at Sep  7 2020 11:11:06
2020-09-07 14:01:50.983945+0800 WebDriverAgentRunner-Runner[1387:217902] ServerURLHere->http://10.0.240.98:8100<-ServerURLHere
2020-09-07 14:01:50.984597+0800 WebDriverAgentRunner-Runner[1387:218027] Using singleton test manager
  • 然后连接看一下,通过电脑打开http://localhost:8100/status看到下面这样就对了:
    status

上面已经实现了在手机上面跑一个wda的程序,下面我们来连接这个手机的server做一些事情。

  • 手机上面的server不能通过外部连接,所以需要用iproxy转换一下,注意这里的端口号其实就是上面命令行打出来的:
命令行输入:iproxy 8100 8100                        
输出:Creating listening port 8100 for device port 8100
waiting for connection

这里的waiting其实是手机在waiting我们的连接

  • 安装python库
pip install facebook-wda
  • 新建个python文件写代码
import wda


def test():
    c = wda.Client('http://localhost:8100')
    print(c.status())
    c.screenshot('screen.png')
    c.session('app bundle id')
    # c.lock()
    # c.unlock()


# Press the green button in the gutter to run the script.
if __name__ == '__main__':
    test()

然后只要你的xcode还在运行wda的runner,你跑起来这个py文件,就会发现自动截屏了~ 但是截屏会存在你的电脑上面:


截屏文件
  • 更多关于wda的指令可以看这个:https://github.com/openatx/facebook-wda

附录:如何识别页面元素(Accessibility Inspector)
入口

你选中自己的手机,然后点右侧的小定位按钮,等待它变为蓝色以后,点击手机上面,会发现有绿色的小方块,也就是你选中了哪一个,Accessibility Inspector正在展示的就是哪个元素的信息:


元素信息

你就可以通过元素的 title 或者 identifier 拿到元素做操作啦~ 例如:

s(id="URL").exists # return True or False

# using id or other query conditions
s(id='URL')

# using className
s(className="Button")

# using name
s(name='URL')
s(nameContains='UR')
s(nameMatches=".RL")

# using label
s(label="label")
s(labelContains="URL")

# using value
s(value="Enter")
s(valueContains="RL")
  • 然而,有些时候我们点击app中的UI组件的时候,会出现点击的组件无法定位或者整个手机屏幕都变绿的情况,这时候我们可以尝试两种解决方法:
  1. 点击inspector中的左移或者右移按钮,这时候会发现app中绿色的位置在移动,继续点击移动到想要选中的组件即可。

  2. 若使用第一种方法依然无法选中想点击的组件,可能是组件之间的层次关系导致的问题。当前inspector绿色色块所在的UI层级,跟想要点击的UI组件不是同一个层级,所以无论如何移动都无法移动到想要点击的组件。

  • 但我们观察inspector工具界面,可以发现界面下半部分可以显示出UI层次结构,并且我们可以点击箭头进入更下级的UI层次Hierarchy,然后我们继续点击inspector的左右移动按钮,就可以选中目标UI组件

  • 特别说明,有时候已经选中目标UI组件,却发现它的Accessibility属性都是空的,这时候就没办法利用Accessibility属性来定位UI组件了,一个可能的办法是可以让开发同学给目标UI组件加上Accessibility属性。

你可能感兴趣的:([iOS] UI自动化测试)