Unit Test、 UI Test

本文详细介绍iOS开发中单元测试与界面测试(UITests)的实施方法,包括使用OCUnit等框架进行单元测试,以及利用Xcode内置工具实现界面自动化测试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

单元测试:


框架有OCUnit、GTM、GHUnit、CATCH、OCMock这些。

OCUnit即用XCTest进行测试)其实就是苹果自带的测试框架,我们主要讲的就是这个。GHUnit是一个可视化的测试框架。(有了它,你可以点击APP来决定测试哪个方法,并且可以点击查看测试结果等。)OCMock就是模拟某个方法或者属性的返回值,你可能会疑惑为什么要这样做?使用用模型生成的模型对象,再传进去不就可以了?答案是可以的,但是有特殊的情况。比如你测试的是方法A,方法A里面调用到了方法B,而且方法B是有参数传入,但又不是方法A所提供。这时候,你可以使用OCMock来模拟方法B返回的值。(在不影响测试的情况下,就可以这样去模拟。)除了这些,在没有网络的情况下,也可以通过OCMock模拟返回的数据。UITests就是通过代码化来实现自动点击界面,输入文字等功能。靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UITests就可以帮助解决这个问题了。

1.新建项目:


18CA997F-4911-4B99-9A83-2AB44A77E8E8.png


2.最简单的测试,注意截图路径的问题
进入到这个类,setUp是每个测试方法调用执行,tearDowLICEcapn是每个测试方法调用执行。testExample是测试方法,和我们新建的没有差别。不过测试方法必须testXXX的格式,且不能有参数,不然不会识别为测试方法。测试方法的执行顺序是字典序排序。
按快捷键Command + U进行单元测试,这个快捷键是全部测试。
testExample方法中输入

    NSLog(@"自定义测试testExample");
    int  a= 3;
    XCTAssertTrue(a == 0,"a 不能等于 0");

点击播放按钮,开始单个方法的测试:


8F503AA4-C630-419A-9F66-C779C81A5581.png


出现如下结果,由于我们断言a是不能等于0的,所以测试没有通过。当然有其它的,用到再看,demo里都有。


8BAD5CD6-7FB7-4626-A1F8-CBC2B6B35E89.png
进行网络请求的测试

使用CocoaPods安装AFNetworking和STAlertView(CocoaPods安装和使用教程
Pofile:

platform :ios, '7.0'
pod 'AFNetworking', '~> 2.5.0'
pod 'STAlertView', '~> 1.0.0'

这时会发现AFNetworking根本没法在单元测试里使用,因为没有找到库,所以我们需要配置一下:


UITestDemo设置.gif

UITestDemo设置2.gif


在Info.plist中添加NSAppTransportSecurity类型Dictionary。 在NSAppTransportSecurity下添加NSAllowsArbitraryLoads类型Boolean,值设为YES。设置位置如下:


iOS9的http安全问题:现在进行异步请求的网络测试,由于测试方法主线程执行完就会结束,所以需要设置一下,否则没法查看异步返回结果。在方法结束前设置等待,调回回来的时候再让它继续执行。(另一种异步函数的单元测试)定义宏如下:

//waitForExpectationsWithTimeout是等待时间,超过了就不再等待往下执行。
#define WAIT do {\\
    [self expectationForNotification:@"RSBaseTest" object:nil handler:nil];\\
    [self waitForExpectationsWithTimeout:30 handler:nil];\\
} while (0)

#define NOTIFY \\
[[NSNotificationCenter defaultCenter]postNotificationName:@"RSBaseTest" object:nil]

增加测试方法testRequest:

-(void)testRequest{
    // 1.获得请求管理者
    AFHTTPRequestOperationManager *mgr = [AFHTTPRequestOperationManager manager];
    mgr.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",nil];

    // 2.发送GET请求
    [mgr GET:@"http://www.weather.com.cn/adat/sk/101110101.html" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
        NSLog(@"responseObject:%@",responseObject);
        XCTAssertNotNil(responseObject, @"返回出错");
        NOTIFY //继续执行
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        NSLog(@"error:%@",error);
        XCTAssertNil(error, @"请求出错");
        NOTIFY //继续执行
    }];
    WAIT  //暂停
}

有时候我们想测试一下整个流程是否可以跑通,比如获取验证码、登录、上传头像,查询个人资料。其实只要输入验证码就可以完成整个测试。这时候就需要用到输入框了,以便程序继续执行。使用了一个第三方的弹出输入框STAlertView,前面已经设置。
STAlertView的使用方法:

        self.stAlertView = [[STAlertView alloc]initWithTitle:@"验证码" message:nil textFieldHint:@"请输入手机验证码" textFieldValue:nil cancelButtonTitle:@"取消" otherButtonTitle:@"确定" cancelButtonBlock:^{
            //点击取消返回后执行
            [self testAlertViewCancel];
            NOTIFY //继续执行
        } otherButtonBlock:^(NSString *b) {
            //点击确定后执行
            [self alertViewComfirm:b];
             NOTIFY //继续执行
        }];
        [self.stAlertView show];



UI Test:

UI Tests的重要性

在实际的开发过程中,随着项目越做越大,功能越来越多,仅仅靠人工操作的方式来覆盖所有测试用例是非常困难的,尤其是加入新功能以后,旧的功能也要重新测试一遍,这导致了测试需要花非常多的时间来进行回归测试,这里产生了大量重复的工作,而这些重复的工作有些是可以自动完成的,这时候UI Tests就可以帮助解决这个问题了

使用方法

第一步:添加UI Tests

如果是新项目,则创建工程的时候可以直接勾选选项,如下图


创建工程

如果是已有的项目,可以通过添加target的方式添加一个UI Tests,点击xcode的菜单,找到target栏

添加target

在Test选项中选择Cocoa Touch UI Testing Bundle


添加target_2

这时候test组件添加成功,它在项目中的位置如下图所示


目录结构
第二步:创建测试代码

手动创建测试代码
打开测试文件,在testExample()方法中添加测试代码


这里写图片描述

如果不知道如何写测试代码,则可以参考自动生成的代码样式

自动生成测试步骤
选择测试文件后,点击录制按钮


这里写图片描述

这时候开始进行操作,它会记录你的操作步骤,并生成测试代码
下图就是在一些操作后自动生成的测试代码


这里写图片描述

这时候可以分析测试代码的语法,以便你自己手动修改或者手写测试代码

开始测试
点击testExample方法旁边的播放按钮,它就开始进行自动测试了,这时候你会看到app在自动操作


这里写图片描述
下面介绍一下测试元素的语法

XCUIApplication:
继承XCUIElement,这个类掌管应用程序的生命周期,里面包含两个主要方法
launch():
启动程序
terminate():
终止程序

XCUIElement:
继承NSObject,实现协议XCUIElementAttributes, XCUIElementTypeQueryProvider
可以表示系统的各种UI元素
exist:
可以让你判断当前的UI元素是否存在,如果对一个不存在的元素进行操作,会导致测试组件抛出异常并中断测试
descendantsMatchingType(type:XCUIElementType)->XCUIElementQuery:
取某种类型的元素以及它的子类集合
childrenMatchingType(type:XCUIElementType)->XCUIElementQuery:
取某种类型的元素集合,不包含它的子类

这两个方法的区别在于,你仅使用系统的UIButton时,用childrenMatchingType就可以了,如果你还希望查询自己定义的子Button,就要用descendantsMatchingType

另外UI元素还有一些交互方法
tap():
点击
doubleTap():
双击
pressForDuration(duration: NSTimeInterval):
长按一段时间,在你需要进行延时操作时,这个就派上用场了
swipeUp():
这个响应不了pan手势,暂时没发现能用在什么地方,也可能是beta版的bug,先不解释
typeText(text: String):
用于textField和textView输入文本时使用,使用前要确保文本框获得输入焦点,可以使用tap()函数使其获得焦点

XCUIElementAttributes协议
里面包含了UIAccessibility中的部分属性
如下图


这里写图片描述

可以方便你查看当前元素的特征,其中identifier属性可用于直接读取元素,不过该属性在UITextField中有bug,暂时不清楚原因

XCUIElementTypeQueryProvider协议
里面包含了系统中大部分UI控件的类型,可通过读属性的方式取得某种类型的UI集合
部分属性截图如下


这里写图片描述

创建Demo

首先创建一个登录页面


这里写图片描述


点击login按钮进行登录验证,点击clear按钮会清除文本
登录成功后可以去到个人信息页面

个人信息页面如下


这里写图片描述


点击modify按钮可以修改个人信息,点击Message按钮可以查看个人消息

最后是消息界面


这里写图片描述
登录页面的测试
  1. 输入一个错误的账号
  2. 验证结果
  3. 关闭警告窗
  4. 清除输入记录
  5. 输入一个正确的账号
  6. 验证结果
  7. 进入个人信息页面
    测试代码如下:
    func testLoginView() {
        let app = XCUIApplication()

        // 由于UITextField的id有问题,所以只能通过label的方式遍历元素来读取
        let nameField = self.getFieldWithLbl("nameField")
        if self.canOperateElement(nameField) {
            nameField!.tap()
            nameField!.typeText("xiaoming")
        }

        let psdField = self.getFieldWithLbl("psdField")
        if self.canOperateElement(psdField) {
            psdField!.tap()
            psdField!.typeText("1234321")
        }

        // 通过UIButton的预设id来读取对应的按钮
        let loginBtn = app.buttons["Login"]
        if self.canOperateElement(loginBtn) {
            loginBtn.tap()
        }

        // 开始一段延时,由于真实的登录是联网请求,所以不能直接获得结果,demo通过延时的方式来模拟联网请求
        let window = app.windows.elementAtIndex(0)
        if self.canOperateElement(window) {
            // 延时3秒, 3秒后如果登录成功,则自动进入信息页面,如果登录失败,则弹出警告窗
            window.pressForDuration(3)
        }

        // alert的id和labe都用不了,估计还是bug,所以只能通过数量判断
        if app.alerts.count > 0 {
            // 登录失败
            app.alerts.collectionViews.buttons["确定"].tap()

            let clear = app.buttons["Clear"]
            if self.canOperateElement(clear) {
                clear.tap()

                if self.canOperateElement(nameField) {
                    nameField!.tap()
                    nameField!.typeText("sun")
                }

                if self.canOperateElement(psdField) {
                    psdField!.tap()
                    psdField!.typeText("111111")
                }

                if self.canOperateElement(loginBtn) {
                    loginBtn.tap()
                }
                if self.canOperateElement(window) {
                    // 延时3秒, 3秒后如果登录成功,则自动进入信息页面,如果登录失败,则弹出警告窗
                    window.pressForDuration(3)
                }
                self.loginSuccess()
            }
        } else {
            // 登录成功
            self.loginSuccess()
        }
    }

这里有几个需要特别注意的点:

  1. 当你的元素不存在时,它仍然可能返回一个元素对象,但这时候不能对其进行操作
  2. 当你要点击的元素被键盘或者UIAlertView遮挡时,执行tap方法会抛异常
    详细实现可参照demo:
    https://github.com/sunljz/demo/tree/master/iOS9/UITestDemo
个人信息页测试
  1. 修改性别
  2. 修改年龄
  3. 修改心情
  4. 保存修改
    测试代码如下:
    func testInfo() {
        let app = XCUIApplication()
        let window = app.windows.elementAtIndex(0)
        if self.canOperateElement(window) {
            // 延时2秒, 加载数据需要时间
            window.pressForDuration(2)
        }

        let modifyBtn = app.buttons["modify"];
        modifyBtn.tap()

        let sexSwitch = app.switches["sex"]
        sexSwitch.tap()

        let incrementButton = app.buttons["Increment"]
        incrementButton.tap()
        incrementButton.tap()
        incrementButton.tap()
        app.buttons["Decrement"].tap()

        let textView = app.textViews["feeling"]
        textView.tap()
        app.keys["Delete"].tap()
        app.keys["Delete"].tap()
        textView.typeText(" abc ")

        // 点击空白区域
        let clearBtn = app.buttons["clearBtn"]
        clearBtn.tap()

        // 保存数据
        modifyBtn.tap()
        window.pressForDuration(2)

        let messageBtn = app.buttons["message"]
        messageBtn.tap();

        // 延时1秒, push view需要时间
        window.pressForDuration(1)

        self.testMessage()
    }

这里需要特别注意以下两点:

  1. textview获取焦点时无法选择焦点的位置
  2. tap事件的触发位置是view的中心,所以当view的中心被遮挡时,要考虑使用其他view来代替
个人消息界面测试
  1. 单元格的点击
    测试代码如下:
    func testMessage() {
        let app = XCUIApplication()
        let window = app.windows.elementAtIndex(0)
        if self.canOperateElement(window) {
            // 延时2秒, 加载数据需要时间
            window.pressForDuration(2)
        }

        let table = app.tables
        table.childrenMatchingType(.Cell).elementAtIndex(8).tap()
        table.childrenMatchingType(.Cell).elementAtIndex(1).tap()

    }

这里需要注意一点:

  1. 暂时无法获取到tableView的元素指针

总结

总的来说,UI Tests只能用于一些基础功能的测试,验证app的功能是否可以正常使用,是否存在崩溃问题。但它也有很多不足之处,编写测试用例的过程非常繁琐,自动生成的代码几乎无法运行,功能单一,很多用例无法覆盖,而且bug很多,大大地限制了UI Tests在实际开发中的应用。希望正式版出来的时候能够修复这些问题,并开放更多的功能。


### 如何添加创建单元测试 #### Java项目的单元测试用例自动生成 对于Java项目而言,可以利用`Auto-Unit-Test-Case-Generator`工具来自动为单个类或是整个模块生成相应的测试用例。此过程涉及安装名为`smart-ut`的组件[^1]。 ```bash # 安装 smart-ut 工具 pip install smart-ut ``` 接着,在命令行界面指定目标路径或者是特定的Java源码文件作为输入参数启动该程序,从而实现自动化地构建初始版本的JUnit风格测试套件。 #### Vue前端应用中的单元测试集成 针对基于Vue框架开发的应用程序来说,引入单元测试机制通常始于配置合适的环境和支持库。通过全局安装Vue CLI并新建工程时勾选必要的选项——特别是“Manually select features”、“Unit Testing”,还有用于执行实际断言逻辑的Jest引擎,便能建立起支持TDD/BDD实践的基础架构[^2]。 ```bash # 全局安装 vue-cli yarn global add @vue/cli # 创建新项目,并选择特性 vue create my_project_name ``` 完成上述设置之后,开发者可以在`tests/unit/`目录下按照视图层(Views)和组件(Components)分类组织各自的`.spec.js`形式描述性文档,确保每一个业务功能都有对应的验证实例覆盖。 #### SwiftUI应用程序内的单元测试管理 当面对采用SwiftUI技术栈打造的产品时,有时会遇到因不当操作而导致预览窗口无法正常加载的情况。对此现象的一种常见修复手段是在IDE内部调整关联资源链接关系:前往左侧导航栏定位至包含有后缀名_tests.swift的目标位置;先移除现有条目后再依照提示重新导入一次相同名称的新副本即可恢复预期行为表现[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值