美文网首页iOS常用开发知识
iOS单元测试之UI测试

iOS单元测试之UI测试

作者: 张聪_2048 | 来源:发表于2022-09-03 15:10 被阅读0次

    一、UI测试简介

    1.1、什么是UITesting

    2015 年,Apple 发布了 UI 自动化测试框架 XCUITest 并集成在 Xcode7 中,而 iOS/macOS UI 自动化测试依赖两个核心技术:XCUITest 和 Accessibility。

    XCUITest 和 Accessibility.png

    XCUITest 是集成在 Xcode 中的测试框架,若想使用 UI 测试功能,可以在创建 iOS 项目时勾选 Include Tests 选项,从而使项目具备自动化测试的能力。而 Accessibility 技术,则是 Apple 官方为视障用户提供的一整套使用 iOS/macOS App 的解决方案。

    Xcode 项目创建 UITests Target 并运行测试,其编译产物 Test App 本质上是一个 Deamon 守护进程,该进程有独立的应用程序生命周期,依靠 XCUIApplication 类型进行管理。UITests 的 Test App 进程在运行时会驱动 Host App(项目的主 Target 产物),并且利用元素审查的相关 API 驱动 Host App 模拟用户行为交互,从而进行 UI 自动化测试。

    对于 Accessibility 技术,开发人员需要注意的是,XCUITest 框架默认并不能将所有视图元素审查到,只会审查到可以被 VoiceOver 功能读取文字的元素。比如,UIButton 和 UILabel,这些视图对于视障用户而言可以通过语音来获知其内容,而对于 UIImageView、 UIView 这种对于视障人士并不友好的 UIKit 视图元素默认是不会审查到的,所以编码时要另行配置 Accessibility 相关属性,以保证其支持 Accessibility 从而在 UI 自动化查询的元素层级中可见。

    基于 XCUITest 框架 和 Accessibility 技术的自动化测试,有利于 App 进行数据一致性校验,但 UI 一致性校验能力较弱。比如,App 可以针对某些数据请求结果或者某个元素是否存在进行校验,而视觉展示效果却仍需要人工介入。

    1.2、使用UITesting

    Using UI Testing:

    • Complements unit testing(补充单元测试)
    • Unit testing more precisely pinpoints failures(单元测试更精确地确定了失败)
    • UI testing covers broader aspects of functionality(UI测试覆盖了函数边界方面)
    • Find the right blend of UI tests and unit tests for your project(找到好的方式融合UI测试和单元测试)

    Candidates for UI Testing(使用UI测试的情况):

    • Demo sequences(一些列Demo)
    • Common workflows(相同的工作流程)
    • Custom views(相同的视图)
    • Document creation, saving, and opening(文档的创建、保存、打开)

    1.3、UI Recording

    通过 UI Recording ,可以将你操作手机的行为记录下来,并且转换成代码,可以帮助你快速生成 UI 测试代码。选中 UI 测试类,你能再下方看到一个小红点,点击小红点开始录制你的交互。

    UIRecording.png

    在你进行交互时,Xcode 会自动转化成代码,你可以借此创建新的测试代码,也可以以此拓展已经存在的测试代码。当然它也不是十分完美,并不是总能如你所愿,还需要你做一些处理,比如说自动生成的代码过于繁琐,你可以用一些更简洁的代码实现。即使这样,UI Recording 也是非常高效的方式。点击下载Demo:ZJHUnitTestDemo

    UIRecording.gif

    二、UI 测试相关的类

    2.1、XCUITest 框架结构

    XCUITest 框架结构图.png

    XCUITest 测试框架 API 主要包含:元素查询(UI Element Queries)相关类型,如 XCUIElementQuery,UI 元素(UI Elements)相关类型,如 XCUIElement,以及测试 App 生命周期类型(Application Lifecycle)类型,如 XCUIApplication。

    2.2、XCUIApplication

    XCUIApplication 代表整个应用,可以用来启动、结束进程,或者传入一些启动参数,最常用的功能是利用 XCUIApplication 实例来查询 UI 上的元素。

    // 返回 UI 测试 Target 设置中选中的 Target Application 的实例
    - (instancetype)init;
    
    // 根据 bundleId 返回一个应用程序实例
    - (instancetype)initWithBundleIdentifier:(NSString *)bundleIdentifier;
    
    // 启动应用程序
    - (void)launch;
    
    // 将应用程序唤醒至前台,在多程序联合测试下会用到 
    - (void)activate;
    
    // 结束一个正在运行的应用程序
    - (void)terminate;
    

    2.3、XCUIElement

    XCUIElement 应用程序中的 UI 控件,控件类型多样,可能是Button,Cell,Window等等。该类实例有很多模拟交互的方法,如tap模拟用户点击事件,swipe模拟滑动事件,typeText:模拟用户输入内容。在 UI 测试中我们需要找到某个空间,可以通过他们的类型来缩小范围。另外还有一种方式通过 Accessibility identifer, label, title 等等方式来定位对应的控件。通过类型加 identifier 的方式来定位的控件元素的方式,可以满足大多数场景。

    XCUIElement示例.png

    也可以通过代码的方式添加Accessibility identifer。

    // 通过代码的方式添加Accessibility identifer
    for (int i = 0; i < 5; i++) {
            UISwitch *swt = [UISwitch new];
            CGFloat pointY = 50 * i + 100;
            swt.center = CGPointMake(self.view.frame.size.width/2, pointY);
            // 添加accessibility标记
            swt.accessibilityLabel = [NSString stringWithFormat:@"swt-%d", I];
            [self.view addSubview:swt];
    }
      
      
     // UI测试获取控件
     XCUIApplication *app = [[XCUIApplication alloc] init];
     [app launch];
     XCUIElement *codeSwt1 = app.switches[@"swt-1"];
     [codeSwt1 tap];
    

    2.4、XCUIElementQuery

    XCUIElementQuery 是一个用来定位控件元素的类,一般是一组符合筛选条件的元素集合。如app.buttons即返回 XCUIElementQuery 实例,是包含了当前所有的button的集合,你可以再通过 XCUIElementQuery的方法做下一步的筛选。

    XCUIElementQuery示例.png

    使用 NSPredicate 为查询条件增加条件

    // 查找所有的 collectionView 的 cell, collectionViews 和 cells 是 XCUIElementQuery 提供的方法
    XCUIElementQuery *cells = app.collectionViews.cells;
    
    // 使用 NSPredicate 为查询条件增加条件
    XCUIElementQuery *cells = [app.collectionViews.cells matchingPredicate:[NSPredicate predicateWithFormat:@"identifier LIKE '?labelPrice?'"]];
    

    三、UI测试示例

    点击下载Demo:ZJHUnitTestDemo

    3.1、使用UI Recording自动生成代码

    新建一个 UI 测试 Target,使用 UI Recording 自动生成代码,或者也可以直接手写。

    UI测试示例UIRecoding.gif

    3.2、修改UI Recording 代码

    UI Recording 的代码识别不出中文,需要手动改下;还会点击两次 tag,删除一个就好。

    /// 修改 UI Recording 生成的代码
    - (void)testLogin2 {
        // 拿到当前application程序
        XCUIApplication *app = [[XCUIApplication alloc] init];
        // 点击 "UITestDemo" 按钮
        [app.staticTexts[@"UITestDemo"] tap];
        
        // 点击账号textField
        [[[[[[[[[app.windows childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:0] tap];
        
        // 点击键盘 shift,切换大小写
        [app.buttons[@"shift"] tap];
        
        // 点击键盘 a
        XCUIElement *aKey = app.keys[@"a"];
        [aKey tap];
    //    [aKey tap]; // 多余tag 需要注释掉
        
        // 点击密码textField
        [[[[[[[[[[[app childrenMatchingType:XCUIElementTypeWindow] elementBoundByIndex:0] childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeOther].element childrenMatchingType:XCUIElementTypeTextField] elementBoundByIndex:1] tap];
        
        // 切换数字键盘
        XCUIElement *moreKey = app.keys[@"more"];
        [moreKey tap];
        
        // 输入1、2、3、4
        XCUIElement *key = app.keys[@"1"];
        [key tap];
        XCUIElement *key2 = app.keys[@"2"];
        [key2 tap];
        XCUIElement *key3 = app.keys[@"3"];
        [key3 tap];
        XCUIElement *key4 = app.keys[@"4"];
        [key4 tap];
        
        // 点击登录按钮
        XCUIElement *button = app.buttons[@"登录"];
        [button.staticTexts[@"登录"] tap];
        
        // 点击键盘删除按钮
        XCUIElement *deleteKey = app.keys[@"delete"];
        [deleteKey tap];
        
        // 点击登录按钮
        [button tap];
        
        // 点击返回按钮
        [app.navigationBars[@"Record List"].buttons[@"登录"] tap];
    }
    

    3.3、精简代码

    UI Recording 生成的代码还不够简练,可以再次对其修改。也可以直接编写,不使用UI Recording生成的

    /// 精简代码
    - (void)testLogin3 {
       // 拿到当前application程序
       XCUIApplication *app = [[XCUIApplication alloc] init];
       
       // 获取 “UITestDemo” 按钮,并点击,跳转到登录页面
       [app.staticTexts[@"UITestDemo"] tap];
       
       // 拿到当前app下的textfeild的搜索器
       XCUIElementQuery *tfQuery = app.textFields;
       // 账号textField
       XCUIElement *accountTF = [tfQuery elementBoundByIndex:0];
       // 密码textField
       XCUIElement *passwordTF = [tfQuery elementBoundByIndex:1];
       
       // 拿到当前app下的button的搜索器
       XCUIElementQuery *btnQuery = app.buttons;
       // 获取登录按钮
       XCUIElement *loginBtn = btnQuery[@"登录"];
    
       // 模拟UI操作
       [accountTF tap]; // 点击账号textField
       [accountTF typeText:@"a"]; // 输入字母a
       [passwordTF tap];// 点击密码textField
       [passwordTF typeText:@"1234"]; // 输入字母123456
       [loginBtn tap]; // 点击登录,提示密码错误
       
       // 获取键盘的删除按钮
       XCUIElement *deleteBtn = app.keys[@"delete"];
       [deleteBtn tap]; // 点击一次删除按钮
       
       // 再次点击登录按钮
       [loginBtn tap]; // 点击登录,成功跳转
    
       // 获取 “Record List” navigationBar,
       XCUIElement *navBarElement = app.navigationBars[@"Record List"];
       // 获取返回按钮
       XCUIElement *backBtn = navBarElement.buttons[@"登录"];
       // 点击返回按钮
       [backBtn tap];
    }
    

    注意:如果某些UI测试失败,请禁用“连接硬件键盘”选项。

    为此,请在模拟器应用程序中选择“ I / O”菜单选项,然后转到Keyboard并取消选中Connect hardware keyboard。 连接硬件键盘后,UI测试似乎无法访问模拟器中的text field

    模拟器弹出键盘.png

    四、UI测试拓展 Tips

    4.1、等待预期

    可以用expectationForPredicate:evaluatedWithObject:handler:方法监听对象属性,当满足NSPredicate条件时,expectation相当于自动fullfill`。如果一直不满足条件,会一直等待直至超时,除此之外还可以用通知和 KVO 的方式实现。

    例如,列表中,新增一个cell数据后,可以监听监听app.cellscount属性,判断cell的个数是否按预期增加,代码如下:

     // 暂存当前 cell 数量
     NSInteger cellsCount = app.cells.count;
     // 设置一个预期 判断 app.cells 的 count 属性会等于 cellsCount+1, 等待直至失败,如果符合则不再等待
     NSPredicate *predicate = [NSPredicate predicateWithFormat:@"count == %d",cellsCount+1];
     [self expectationForPredicate:predicate evaluatedWithObject:app.cells handler:nil];
        
     // 执行添加操作,或者网络请求等异步操作
     [self addCellData];
     
     // 等待实现预期,这里等到10s
     [self waitForExpectationsWithTimeout:10 handler:nil];
    
    

    4.2、多应用联合测试

    多应用联合测试时,依赖XCUIApplication类的以下 2 个方法:

    • initWithBundleIdentifier:
    • activate

    前者可以根据 BundleId 获取其他 App 的实例,让我们可以启动其他 App。后者可以让 App 从后台切换至前台,在多应用间切换。简单实现代码如下:

    - (void)testExample {
        // 返回 UI 测试 Target 设置中选中的 Target Application 的实例
        XCUIApplication *app = [[XCUIApplication alloc] init];
        
        // 使用 BundleId 获得另外一个 App 实例:需要先创建另个测试app
        XCUIApplication *anotherApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"zjh.ZJHUnitTestDemo2"];
    
        // 先启动我们的主 App
        [app launch];
        
        // 做一系列测试1
        [app.staticTexts[@"UITestDemo"] tap];
        [app.navigationBars[@"登录"].buttons[@"Home"] tap];
    
        sleep(2);
            
        // 启动另一个 App
        [anotherApp activate];
        
        sleep(2);
        
        // 回到我们的主 App (在 App 未启动的情况下调 activate 会让 App 启动)
        [app activate];
        
        // 做一系列测试2
        [app.staticTexts[@"UITestDemo"] tap];
        [app.navigationBars[@"登录"].buttons[@"Home"] tap];
    }
    

    4.3、截屏

    在 UI 测试中有 2 种类型支持通过代码截屏,分别是XCUIElementXCUIScreen

    // 获取一个截屏对象
    XCUIScreenshot *screenshot = [app screenshot];
    
    // 实例化一个附件对象 并传入截屏对象
    XCTAttachment *attachment = [XCTAttachment attachmentWithScreenshot:screenshot];
    
    // 附件的存储策略 如果选择 XCTAttachmentLifetimeDeleteOnSuccess 则测试成功的情况会被删除
    attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
    
    // 设置一个名字 方便区分
    attachment.name = @"MyScreenshot";
    
    [self addAttachment:attachment];
    

    在测试结束后,可以在 Report 导航栏中查看截图:

    查看截图.png

    除此之外 Xcode 提供了自动截图的功能,可以帮助我们在每一个交互操作之后自动截图。此功能会产生大量截图,需要谨慎使用,一般情况最好勾选Delete when each test succeeds,需要在 Edit Scheme -> Test -> Options 中开启。

    4.4、被测试 app 如何判断正在进行 UI Test

    在启动 app 时增加一个启动参数,在 app 中读取。

    // 测试代码
    XCUIApplication *app = [[XCUIApplication alloc] init];
    app.launchEnvironment = @{@"isUITest" : @YES};
    [app launch];
    
    // app 代码
    + (BOOL)isUITesting {
        NSDictionary *environment = [[NSProcessInfo processInfo] environment];
        return [environment[@"isUITest"] boolValue];
    }
    

    五、Accessibility Inspector简介

    5.1、使用 Accessibility Inspector

    Accessibility Inspector 辅助功能检查器,通过辅助功能检查器,您可以识别应用程序中无法访问的部分。它提供了有关如何访问它们的反馈,并模拟画外音,以帮助您识别画外音用户的体验。观看在辅助功能检查器中完全调试的应用程序的实时演示,并了解如何利用这个强大的工具使您的应用程序更适合每个人。

    前文中提到 Apple 对于视图元素会默认审查能够通过 VoiceOver 播放文字的视图元素,而对于 UIImageView、UIView 这种默认不支持 Accessibility 功能的需要配置相关特性,而开发人员在开发过程中可以通过 Accessibility Inspector 查看不同进程的 Accessibility 元素层级,该应用可以审查 iOS 和 macOS 的元素。

    选择 Xcode 的图标菜单并选择 Open Developer Tool 选项,点击 Accessibility Inspector 即可开始使用。

    打开Accessibility Inspector.png

    当我们没有设置 isAccessibilityElement 属性时,在 Accessibility 元素层级结构中就无法看到 UIImageView 和 UIView 元素,只能看到 “t我是Button” 和“我是Label”。而当我们将 UIView 的 isAccessibilityElement 属性设置为 YES 时, UIView 元素才能在元素层级中可见,UIImageView默认还是看不见。设置代码如下:

        NSArray *nameArr = @[@"我是Button", @"我是Label", @"我是View", @"我是Image"];
            
            if (i == 0) { // 按钮
                UIButton *btn = [[UIButton alloc] initWithFrame:btnF];
                [btn setTitle:nameArr[i] forState:UIControlStateNormal];
                temView = btn;
            } else if (i == 1) { // label
                UILabel *lab = [[UILabel alloc] initWithFrame:btnF];
                lab.text = nameArr[i];
                lab.textAlignment = NSTextAlignmentCenter;
                temView = lab;
            } else if (i == 2) { // view
                UIView *view = [[UIView alloc] initWithFrame:btnF];
                view.isAccessibilityElement = YES; // 将 UIView  的 isAccessibilityElement 属性设置为 YES 
                view.accessibilityIdentifier = nameArr[I];
                temView = view;
            } else if (i == 3) { // 图片
                UIImageView *imgView = [[UIImageView alloc] initWithFrame:btnF];
                imgView.image = [UIImage imageNamed:@"avatar"];
                imgView.accessibilityIdentifier = nameArr[i];
                temView = imgView;
            }
    
    Accessibility Inspector使用.png

    5.2、Accessibility 相关属性

    @property (nullable, nonatomic, copy) NSString *accessibilityLabel;
    

    accessibilityLabel 属性可以解决绝大部分的 Accessibility 问题,当光标将焦点放在设置该属性的元素师时,它的内容可由 VoiceOver 读取的人类可读的字符串。但如果不是需要被视障用户获知的视图元素,仅用于自动化测试,就可以不用设置该属性。

    @property(nullable, nonatomic, copy) NSString *accessibilityIdentifier API_AVAILABLE(ios(5.0));
    

    accessibilityIdentifier 属性不会被 VoiceOver 诵读,而是面向开发人员的字符串,可在不希望用户操作 accessibilityLabel 的情况下使用。

    @property (nonatomic) BOOL isAccessibilityElement;
    

    如果 isAccessibilityElement 未设置为 true,那么这个视图将不会在 Accessibility 视图层次结构中可见。

    • The default value for this property is false unless the element is a standard UIKit control, in which case, the value is true. —— Apple Documentation

    另外,根据 Apple 官方中的介绍 UIControl 的子类的 isAccessibilityElement 属性都默认设置为 true。

    5.3、编写测试用例

    - (void)testExample {
        XCUIApplication *app = [[XCUIApplication alloc] init];
        [app.staticTexts[@"Accessibility Demo"] tap];
        
        XCUIElement *button = app.buttons[@"我是Button"];
        XCTAssertTrue(button.exists);
        XCUIElement *label = app.staticTexts[@"我是Label"];
        XCTAssertTrue(label.exists);
        XCUIElement *view = app.otherElements[@"我是View"];
        XCTAssertTrue(view.exists);
        XCUIElement *imgview = app.images[@"我是Image"];
        XCTAssertTrue(imgview.exists);
    }
    

    六、三方框架KIF简介

    6.1、KIF简介

    KIF 的全称是Keep it functional。它是一个建立在XCTest的UI测试框架,通过accessibility来定位具体的控件,再利用私有的API来操作UI。由于是建立在XCTest上的,所以你可以完美的借助XCode的测试相关工具。

    6.2、pod引入框架

    pod引入框架.png
    • 必须将Target设置为Unit Test,根据GitHub官方说明。不要设置成UI Test 项目了,我这就设错了,调了大半天才找到原因

    • 查看GitHub的ReadMe,使用Cocoapod进行安装,命令如下(在Debug模式下才生效)

    • KIF一定要放到测试项目下面

      target 'ZJHKIFUnitTestDemoTests' do
          pod 'KIF', :configurations => ['Debug']
        end
      

    6.3、简单使用

    KIF使用示例.png

    更多接口介绍,可参考:KIF API中文翻译



    参考链接:
    iOS 单元测试和 UI 测试快速入门:https://juejin.cn/post/6844903744170098695
    iOS UI 自动化测试原理以及在 Trip.com 的应用实践:https://www.51cto.com/article/686176.html
    iOS UI Testing 指北:https://nixwang.com/2018/09/30/ios-ui-testing/

    相关文章

      网友评论

        本文标题:iOS单元测试之UI测试

        本文链接:https://www.haomeiwen.com/subject/vnkmnrtx.html