iOS 自动化测试 UI Automation

auto.jpg

iOS提供了一个框架UIAutomation,可用来实现自动化测试,可以通过这个框架自定义一些列操作,自动的运行在真机设备或者xcode的模拟器上。

UIAutomation框架的测试方式属于非注入式的,测试内容和最终上线内容一致,不需要源码,但是功能会有所受限。注入式的自动化测试需要用到第三方的工具。本文主要用到的是前者的方式。

测试环境需要用到instruments,xcode自带的工具。本文的例子为直接在模拟器上进行测试。

先用个现成的项目来展示下大概的用法。打开个博主自己写的跑步软件,在Xcode栏->选则Produce->Profile,之后选择Automation

2.jpg

进入Automation的界面

8.jpg

这个界面的更多细节在下面的Demo中解释,先看到界面的黑色区域,为编写脚本代码的地方,在此处添加脚本后点击左上角的红色圆形按钮,运行起App后便会自动运行脚本的内容。

在App运行的过程中可以实时修改脚本代码,并点击代码编辑区域下方的“播放”按钮,能实时运行脚本。

UIAutomation是通过层级访问的方式来定位到某一个具体元素的,苹果提供了一个logElementTree()的方法,打印控件树。通过运行这个方法,能看到App当前界面UI的层级。打印层级的脚本代码如下:

var target = UIATarget.localTarget();
target.logElementTree();

App的界面如图

10.jpg

选中Trace Log展开打印的结果,可以看到界面上的各种控件信息,如标签、按钮的文本位置等。

9.jpg

接下来通过脚本实现一个最基本的操作,自动点击“开始跑步”按钮开始跑步的功能。

“开始跑步”按钮在展开层级上的信息,可以看到为一个UIAButtonname属性为“开始跑步”

11.jpg

脚本的代码实现如下:

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();

var runBtn = window.buttons()["开始跑步"];
runBtn.tap();

UIATarget 对象代表待测应用所在环境的最高层级UI,在这里localTarget()表示运行app的这台iPhone设备。

UIAApplication 对象代表app层级的UI,这里通过frontMostApp()方法得到的对象,就是指正在运行的app。

UIAWindow对象代表app中window层级的UI,这里通过mainWindow()方法得到的对象,指当前app中的主窗体,一个app的当前界面通常只会有一个主窗体。

实际项目中,不同元素的差异都是从window层级开始的,在window层级往上,都是一样的。

自己编写测试代码,具体参考官方文档 UI Automation JavaScript Reference for iOS,对App的各个操作最终都是通过这些api来实现的。

buttons()方法用来获取界面上的所有按钮元素,然后通过name属性获取对应的按钮。name属性默认值为按钮的文本,也可以通过设置UIButtonaccessibilityLabel属性来改变name的值。也可通过IB修改

12.jpg

tap()方法既向按钮做一个点击操作,若要模拟其他操作可参考UIAButton 提供的接口,不同的控件系统都提供了对应的操作接口。

当前App正在模拟器中运行,加入脚本的代码后直接点击下方的“播放”按钮,App进入跑步功能。

13.jpg

自动操作的实现大概就是这样。当然,现实的自动化测试中肯定会涉及到更复杂的操作流程,可以通过脚本调用各种控件对应的事件操作接口,自定义操作的顺序,来实现自动化测试流程。

<h3>一个简单的Demo</h3>

上边展示用到的是项目的代码,接下来实现一个简单的Demo,实现一个最基本的脚本自动测试。

具体实现内容如下图所示,界面上有若干按钮,按钮初始化标签值为0,编写脚本随机在界面上进行点击事件,当点中按钮时,对应的按钮标签值加1。这种模拟界面上随机点击操作事件的方式可以用来测试App功能的稳定性和健壮性。最后附demo的完整源码。

1.jpg

首先新建一个工程,命名为AutoTest

创建一个UIButton的子类MyButton,按钮被点击时标签的值自动递增1,这个功能放在MyButton类中实现。

MyButton .h文件

#import <UIKit/UIKit.h>

@interface MyButton : UIButton

- (void)click;

@end

按钮被点击时调用click方法,标签值递增。

MyButton .m文件

#import "MyButton.h"

@interface MyButton ()

@property (nonatomic, assign) NSInteger count;

@end

@implementation MyButton

- (id)init
{
    self = [super init];
    if (self)
    {
        self.count = 0;
        [self setTitle:[NSString stringWithFormat:@"%@", @(self.count)] forState:UIControlStateNormal];
        [self setTitleColor:[UIColor grayColor] forState:UIControlStateNormal];
    }
    
    return self;
}

- (void)click
{
    self.count ++;
    [self setTitle:[NSString stringWithFormat:@"%@", @(self.count)] forState:UIControlStateNormal];
}

@end

重载init方法,初始化时给count属性附初始值1。

在创建工程自动生成的试图控制器类ViewController中添加按钮。

引入自定义类的头文件

#import "MyButton.h"

定义clickBtn:方法,作为按钮点击时调用的方法。

- (void)clickBtn:(id)sender
{
    MyButton *btn = (MyButton *)sender;
    [btn click];
}

定义addButtons方法,向界面添加按钮。

- (void)addButtons
{
    CGFloat w = CGRectGetWidth(self.view.frame);
    CGFloat h = CGRectGetHeight(self.view.frame);
    
    CGFloat distance = 20;      // 按钮之间、按钮和屏幕边缘之间的间距
    NSInteger rowCount = 9;    // 行数
    NSInteger columnCount = 5;  // 列数
    
    CGFloat btnW = (w - distance * (columnCount + 1)) / columnCount;    // 按钮宽度
    CGFloat btnH = (h - distance * (rowCount + 1)) / rowCount;          // 按钮高度
    
    // 将按钮添加到界面上
    for (NSInteger i = 0; i < (rowCount * columnCount); i++)
    {
        CGFloat x = (i % columnCount) * (btnW + distance) + distance;
        CGFloat y = (i / columnCount) * (btnH + distance) + distance;
        CGRect frame = CGRectMake(x, y, btnW, btnH);
        
        MyButton *btn = [[MyButton alloc] init];
        btn.accessibilityLabel = [NSString stringWithFormat:@"%@", @(i)];
        btn.frame = frame;
        
        [btn addTarget:self action:@selector(clickBtn:) forControlEvents:UIControlEventTouchUpInside];
        [self.view addSubview:btn];
    }
}

一共有9行,每行5个按钮,每个按钮尺寸大小相等,按钮与按钮之间、按钮与屏幕边缘之间的具体固定。

在视图控制器的生命周期viewDidAppear函数里将按钮添加上。

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    [self addButtons];
}

按钮的布局需要用到视图控制器view的大小,而view的大小在viewDidAppear:时才确定下来,所以在这个时候才将按钮添加到界面上。

至此Demo App方面的代码实现完成,跑起来界面效果如上图所示。

<h3>编写自动化脚本</h3>

运行profile:xcode工具栏->Product->Profile,选择Automation选项

2.jpg

之后进入界面,如下图所示,图中标记几处需要留意的地方。

3.jpg

中间的黑色区域为编写JavaScript脚本的地方,系统会自己默认自动添加了行代码

var target = UIATarget.localTarget();

编码区域上方的Script按钮点击展开还会有另外两个选项:Trace LogEditor Log,分别用来打印对应的记录。

左上角的红色圈按钮,点击会运行程序。

图下方的三个按钮,第一个三角形图标的按钮,可以在程序已经跑起来的时候,实时更改脚本并运行;第二个红色圆形按钮用来录制操作,能将对App的每一次操作生成对应的脚本语句,下次直接跑脚本就能将之前的操作自动重复一次了。

右边还有按钮能导入导出脚本,给脚本文件改名,暂停继续脚本的运行等。

<h3>脚本的录制</h3>

点击下方的录制按钮,这时候程序运行起来,进行一系列操作,例如,从左上角开始,将外围的一圈按钮按顺时针顺序依次点击。结果如图:

4.jpg

在点击的过程中会发现,每次点击操作在脚本编辑区域都会生成一行代码

5.jpg

代码生成的速度会比实际的操作稍微延时。

开始以为生成的代码都是一样的都是

target.frontMostApp()

所以就很好奇它是怎样具体去区分每一个按钮的呢?直到无意中把代码区域的拖动全选了下才发现。。。

7.jpg

突然感受到深深的恶意...-_-

点击按钮之间的空白区域也会生成对应的脚本代码,包含了具体的点击位置数据

6.jpg

录制完成之后点击旁边的正方形图标按钮。之后可以点击左上角红色圆形按钮,这时候程序运行起来,会自动运行刚刚录制所生成的脚本代码,这时候能看到看看点击了一轮按钮的操作又自己跑了一遍。

脚本的录制方式大概情况就是这样。

<h3>随机点击操作</h3>

接下来用非录制的方式创建个脚本,用来在界面上进行随机位置的点击。

随机点击屏幕脚本

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();

var rect = target.rect();
var w = rect.size.width;
var h = rect.size.height;

for (;;)
{
    var x = Math.random() * w;
    var y = Math.random() * h;
    target.tap({x, y});
}

target.rect()获取的是屏幕界面的rect,包含了屏幕的尺寸大小。

通过for无限循环来产生随机的点击事件,点击的位置为屏幕范围内的任意点。targettap()方法传递位置参数,并在界面上进行点击操作。

运行节本的过程中能够看到界面上按钮的值不停递增

![image1]({{ site.url }}/assets/AutoTest/14.jpg)

instrument上也不断有打印输出

15.jpg

若想控制脚本点击事件的间隔,可以再每次点击之后加上targetdelay()方法,参数为秒,延迟一定的时间后再继续运行脚本。

延迟2秒再继续

target.delay(2);

可以输出log内容

UIALogger.logMessage(msg);

<h3>每一次点击操作都针对按钮</h3>

有时候App上的按钮可能只有几个,全屏的随机点击事件点击到按钮的概率比较低,此时需要每次点击事件都针对按钮进行操作,脚本可以这样处理

var target = UIATarget.localTarget();
var app = target.frontMostApp();
var window = app.mainWindow();

var count = window.buttons().toArray().length;
var msg = "total buttons: " + count.toString();
UIALogger.logMessage(msg);

for (;;)
{
    var index = Math.floor(Math.random() * count);
    var btn = window.buttons()[index];
    btn.tap();
    target.delay(1);
}

windowbuttons()返回的是它所包含的所有按钮,为UIAElementArray类型,转换成Array后计算元素的数量。

window.buttons()[index] index为按钮的索引值,可以通过下标索引的方式来获取其中的某一个按钮。

获取到按钮后通过tap()产生点击事件。每次操作间有个1秒的延迟。

<h3>结</h3>

自动化测试的方式基本用法如上所示。Demo展示的内容比较基本,还有各种更复杂的玩法和细节改天有空再研究了。还有一些第三方提供的自动化工具之类的,以后专门再写一篇文章。

脚本的代码直接复制粘贴上即可跑起来。

工程源码地址

转载请保留原文地址:https://moshuqi.github.io/ios/2016/03/08/iOS自动化测试-Automation.html

<h4>完。</h4>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容