首页/新闻资讯/正文详情

使用 Quick 测试 OS X 与 iOS 应用:UIViewController 生命周期、Storyboard 与 UIControl 事件实战

发布时间:2026/9/24 22:19:18 来源:云帆数科 栏目:资讯中心
使用 Quick 测试 OS X 与 iOS 应用:UIViewController 生命周期、Storyboard 与 UIControl 事件实战
测试开发工具【免费下载链接】QuickThe Swift (and Objective-C) testing framework.项目地址https://gitcode.com/gh_mirrors/qu/Quick点击查看免费下载本文面向已掌握 Quick 基础用法的开发者系统讲解如何用 Quick以及 Nimble 断言库为UIViewController及其子类编写测试包括手动触发视图控制器生命周期事件、从故事板Storyboard初始化控制器、通过程序发送UIControl事件模拟按钮点击以及等待异步操作完成的标准姿势。读完本文你将能在不依赖真实 UI 交互的前提下用 Swift 与 Objective-C 两套 DSL 写出可稳定运行、可被 XCTest 正确统计的应用层测试。从测试普通类到测试 UIKit 类在项目中添加测试 一文覆盖了为任意 Objective-C 或 Swift 函数、类编写测试所需的全部准备工作Target 配置、测试文件组织等。但UIViewController及其子类与普通类有一个本质差异生命周期由 UIKit 驱动。当一个视图控制器被真正呈现到应用窗口时UIKit 会自动按顺序触发viewDidLoad、viewWillAppear、viewDidAppear等事件而在单元测试中控制器通常只是被init出来从未被呈现因此这些事件一概不会发生。如果你的被测代码把业务逻辑挂在生命周期回调里例如在viewDidLoad中给标签赋初值直接测试就会得到空值。本文接下来介绍的三类技巧正是为了解决这个问题手动触发生命周期、初始化故事板中的控制器、程序化发送控件事件。触发UIViewController生命周期事件在测试UIViewController时你需要自己触发这些生命周期事件。共有三种方式访问UIViewController.view这会触发类似viewDidLoad()的事件。使用UIViewController.beginAppearanceTransition()可触发大部分生命周期事件。注意从 iOS SDK 13.0 起该方法不再触发viewDidAppear()。直接调用方法如UIViewController.viewDidLoad()或UIViewController.viewWillAppear()。Swift 示例下面的BananaViewControllerSpec依次演示了三种方法。测试对象是一个虚构的BananaViewController来自示例模块BananaApp它有一个bananaCountLabel标签和moreButton按钮// Swift import Quick import Nimble import BananaApp class BananaViewControllerSpec: QuickSpec { override class func spec() { var viewController: BananaViewController! beforeEach { viewController BananaViewController() } describe(.viewDidLoad()) { beforeEach { // Method #1: Access the view to trigger BananaViewController.viewDidLoad(). let _ viewController.view } it(sets the banana count label to zero) { // Since the label is only initialized when the view is loaded, this // would fail if we didnt access the view in the beforeEach above. expect(viewController.bananaCountLabel.text).to(equal(0)) } } describe(the view) { beforeEach { // Method #2: Triggers .viewDidLoad(), .viewWillAppear(), and .viewDidAppear() events. viewController.beginAppearanceTransition(true, animated: false) viewController.endAppearanceTransition() } // ... } describe(.viewWillDisappear()) { beforeEach { // Method #3: Directly call the lifecycle event. viewController.viewWillDisappear(false) } // ... } } }Objective-C 示例同样的逻辑用 Quick 的 Objective-C DSL 编写使用QuickSpecBegin/QuickSpecEnd宏包裹beforeEach与describe均为宏语法// Objective-C import Quick; import Nimble; #import BananaViewController.h QuickSpecBegin(BananaViewControllerSpec) __block BananaViewController *viewController nil; beforeEach(^{ viewController [[BananaViewController alloc] init]; }); describe(-viewDidLoad, ^{ beforeEach(^{ // Method #1: Access the view to trigger -[BananaViewController viewDidLoad]. [viewController view]; }); it(sets the banana count label to zero, ^{ // Since the label is only initialized when the view is loaded, this // would fail if we didnt access the view in the beforeEach above. expect(viewController.bananaCountLabel.text).to(equal(0)) }); }); describe(the view, ^{ beforeEach(^{ // Method #2: Triggers .viewDidLoad(), .viewWillAppear(), and .viewDidAppear() events. [viewController beginAppearanceTransition:YES animated:NO]; [viewController endAppearanceTransition]; }); // ... }); describe(-viewWillDisappear, ^{ beforeEach(^{ // Method #3: Directly call the lifecycle event. [viewController viewWillDisappear:NO]; }); // ... }); QuickSpecEnd三种方式的取舍方式触发的典型事件适用场景注意事项访问view属性viewDidLoad及懒加载的视图层级初始化只关心视图加载后的初始状态若标签等 UI 元素在viewDidLoad中才被创建这是唯一可靠触发其初始化的方式beginAppearanceTransitionendAppearanceTransitionviewWillAppear、viewDidAppeariOS 13 前、viewWillDisappear等需要模拟控制器即将出现/已经出现的完整外观转换iOS SDK 13.0 起不再触发viewDidAppear()直接调用生命周期方法精确指定的单个方法只验证某个回调内部的逻辑分支最直接但完全不经过 UIKit 调度一个常见误区是在it闭包里才访问viewController.view。从上面的 Swift 示例可以看到访问动作应放在外层beforeEach中——因为断言如标签文本等于0依赖视图加载这个副作用而beforeEach保证在每条用例执行前完成准备。初始化在故事板Storyboard中定义的视图控制器如果视图控制器是在故事板中定义的直接init得到的实例不会包含任何已连接的 IBOutlet 与布局。正确的做法是先在 Interface Builder 中为它分配一个Storyboard IDIdentity Inspector → Storyboard ID然后通过UIStoryboard按 ID 实例化。为视图控制器分配 ID 后就可以在测试中初始化了// Swift var viewController: BananaViewController! beforeEach { // 1. Instantiate the storyboard. By default, its name is Main.storyboard. // Youll need to use a different string here if the name of your storyboard is different. let storyboard UIStoryboard(name: Main, bundle: nil) // 2. Use the storyboard to instantiate the view controller. viewController storyboard.instantiateViewControllerWithIdentifier( BananaViewControllerID) as! BananaViewController }// Objective-C __block BananaViewController *viewController nil; beforeEach(^{ // 1. Instantiate the storyboard. By default, its name is Main.storyboard. // Youll need to use a different string here if the name of your storyboard is different. UIStoryboard *storyboard [UIStoryboard storyboardWithName:Main bundle:nil]; // 2. Use the storyboard to instantiate the view controller. viewController [storyboard instantiateViewControllerWithIdentifier:BananaViewControllerID]; });几个实战要点故事板名称默认是Main如果你的文件叫别的名字例如Main_iPhone.storyboard必须传入对应的字符串。bundle: nil表示使用主 bundle。如果故事板位于测试 target 之外的 framework 中应传入对应的Bundle。按 ID 实例化返回的是Any/id类型需要做一次类型转换Swift 中使用as!需保证 ID 与类型匹配。通过instantiateViewController(withIdentifier:)得到的控制器此时尚未加载视图后续仍需要按上一节的方法触发viewDidLoad等事件IBOutlet 才会真正连接到视图层级。触发 UIControl 事件如点击按钮按钮以及其他继承自UIControl的 UIKit 类定义了一些方法使我们能够通过程序代码发送控制事件例如模拟一次点击。这样测试就不需要真实的触摸事件而sendActionsForControlEvents:/sendActions(for:)会同步调用按钮上注册的所有 target-action行为与真实点击一致。// Swift describe(the more bananas button) { it(increments the banana count label when tapped) { viewController.moreButton.sendActionsForControlEvents( UIControlEvents.TouchUpInside) expect(viewController.bananaCountLabel.text).to(equal(1)) } }// Objective-C describe(the more bananas button, ^{ it(increments the banana count label when tapped, ^{ [viewController.moreButton sendActionsForControlEvents:UIControlEventTouchUpInside]; expect(viewController.bananaCountLabel.text).to(equal(1)); }); });这段代码验证了点击moreButton后bananaCountLabel文本变为1。在实际项目中控制事件还可能是.TouchDown、.ValueChanged如UISwitch、UISlider、.EditingChanged如UITextField等思路完全一致把要模拟的UIControl.Event传给sendActions。需要说明的是文档中的sendActionsForControlEvents/UIControlEvents.TouchUpInside是较早期 Swift 版本的 API 命名现代 Swift 的等价写法是viewController.moreButton.sendActions(for: .touchUpInside)语义相同。等待异步操作XCTestExpectation 与 QuickSpec.current应用测试经常需要等待动画、网络请求等在后台队列或主运行循环的下一轮中完成的异步操作。顺带一提大多数情况下测试应当对网络调用做 stub而不是真的使用网络。XCTest 处理异步操作的标准机制是expectation预期。Quick 支持它但要格外小心不要使用self获取XCTest实例包括创建 expectation 和等待 expectationit(makes a network call) { // WRONG: dont use self.expectation in Quick let expectation self.expectation(description: network call) URLSession.shared.dataTask(with: URL(string: https://example.com)!) { _ in expectation.fulfill() }.resume() // WRONG: dont use self.waitForExpectations in Quick self.waitForExpectations(timeout: 1) }为什么不能用self这背后是 Quick 的运行机制。查看仓库源码 Sources/Quick/QuickSpec.swift 可以看到Quick 为每个it动态注册一个实例方法运行示例example.run()时会先把example赋给spec实例同时把QuickSpec.current指向它示例执行完毕后再把QuickSpec.current置回nil。也就是说当 Quick 执行你的spec()函数收集示例时它运行在一个替身dummyXCTest实例上真正的XCTest直到那些it闭包真正运行时才出现。在你的it闭包里self捕获的正是那个替身实例。用这个替身实例操作 expectation 有两个层面的问题有时会直接报 Questionable API usage可疑 API 用法或 API violationAPI 违规错误有时看起来能跑但它绕过了 XCTest 对常见错误例如忘记调用waitForExpectations()的检查。正确解法使用QuickSpec.current解决方案是使用QuickSpec.current它返回当前正在执行的 XCTest 实例// Swift it(makes a network call) { let expectation QuickSpec.current.expectation(description: network call) URLSession.shared.dataTask(with: URL(string: https://example.com)!) { _ in expectation.fulfill() }.resume() QuickSpec.current.waitForExpectations(timeout: 1) }// Objective-C it(makes a network call, ^{ XCTestExpectation *expectation [QuickSpec.current expectationWithDescription:network call]; NSURLSessionTask *task [NSURLSession.sharedSession dataTaskWithURL: [NSURL URLWithString:https://example.com] completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) { [expectation fulfill]; }]; [task resume]; [QuickSpec.current waitForExpectationsWithTimeout:1 handler:NULL]; });从源码理解QuickSpec.current与currentSpec()QuickSpec.current在 Swift 侧定义为public private(set) static var current: QuickSpec!见 Sources/Quick/QuickSpec.swift#L21并在example属性的didSet观察器中自动赋值Sources/Quick/QuickSpec.swift#L23-L27只要某个示例开始运行current就会被更新为该QuickSpec实例示例结束或跳过后置回nil。Objective-C 侧的实现位于 Sources/QuickObjectiveC/QuickSpec.m#L10用一个静态全局变量currentSpec配合 (QuickSpec *)current类方法暴露Sources/QuickObjectiveC/QuickSpec.m#L71-L73赋值时机与 Swift 侧完全一致。Quick 还提供了统一入口currentSpec()它在beforeSuite/afterSuite闭包中特别有用——因为这两个钩子可能同时涉及同步与异步 spec而QuickSpec.current与AsyncSpec.current分别只覆盖其中一种。其实现见 Sources/Quick/CurrentSpec.swift#L9-L10public func currentSpec() - XCTestCase? { QuickSpec.current ?? AsyncSpec.current }注意currentSpec()不适用于标准 XCTest 测试——对用纯XCTest编写的测试用例它返回nil。这一点在仓库的测试 Tests/QuickTests/QuickTests/FunctionalTests/CurrentSpecTests.swift#L80-L87 中有明确验证XCTestCurrentSpecTests中三个断言currentSpec()、QuickSpec.current、AsyncSpec.current均期望为nil。异步 specAsyncSpec同样提供AsyncSpec.current见 Sources/Quick/Async/AsyncSpec.swift#L16并且CurrentSpecTests中也覆盖了同步/异步 spec 互相查询对方current均为nil的隔离性以及示例未运行时current为 nil、运行时返回当前 spec的行为Tests/QuickTests/QuickTests/FunctionalTests/CurrentSpecTests.swift#L6-L38。因此同步 spec 的it内使用QuickSpec.current异步 spec 的it内使用AsyncSpec.currentbeforeSuite/afterSuite钩子中使用currentSpec()。Nimble 的备选方案toEventuallyNimble 的expect(…).toEventually(…)同样可以辅助测试异步操作写法更简洁it(makes a network call) { var networkCallCompleted false URLSession.shared.dataTask(with: URL(string: https://example.com)!) { _ in networkCallCompleted true }.resume() expect(networkCallCompleted).toEventually(beTrue()) }但这个方案存在几个明显的局限不是线程安全的如果被测条件不在主线程上满足你需要自己做同步缺少健全性检查不会检查是否有未等待的条件无法聚合多个期望不能在一次等待中同时满足多个断言速度更慢fulfill会立即触发成功判定而toEventually()需要轮询等待。不过在约束都不成立例如只需等待一个简单的主线程标志位的场景下toEventually()能让测试代码显著更简洁是合理的取舍。小结测试UIViewController时UIKit 不会替你触发生命周期事件需通过访问view、beginAppearanceTransition/endAppearanceTransition或直接调用生命周期方法来手动触发。故事板中定义的控制器要先用 Storyboard ID 标记再用UIStoryboard(name:bundle:)instantiateViewController(withIdentifier:)初始化。按钮等UIControl子类可用sendActions(for:)旧写法sendActionsForControlEvents:程序化发送控制事件无需真实触摸。在 Quick 中等待异步操作务必用QuickSpec.current/AsyncSpec.current钩子中用currentSpec()切勿使用self捕获的替身 XCTest 实例也可按需选用 Nimble 的toEventually()。本文对应的完整入门准备见 在项目中添加测试Quick 的示例与分组 DSL 可参考 QuickExamplesAndGroups.mdNimble 断言语法见 NimbleAssertions.md异步 spec 的完整能力见 AsyncAwait.md。赞分享测试开发工具【免费下载链接】QuickThe Swift (and Objective-C) testing framework.项目地址https://gitcode.com/gh_mirrors/qu/Quick点击查看免费下载相关推荐Sails 应用事件Application Events深度指南hook 生命周期事件、sails.after 与测试实践Sails 应用事件Application Events深度指南hook 生命周期事件、sails.after 与测试实践 Sails 应用实例本身就是一后端uni-app x iOS UTS 插件 UTSiOSHookProxy 详解应用程序生命周期回调协议与实战指南uni app x iOS UTS 插件 UTSiOSHookProxy 详解应用程序生命周期回调协议与实战指南 UTSiOSHookProxy 是 uni示例工程前端移动开发跨平台FastStream 事件测试指南用 TestApp 在测试中触发应用生命周期钩子FastStream 事件测试指南用 TestApp 在测试中触发应用生命周期钩子 在 FastStream 中绝大多数测试围绕 订阅者测试 https:/后端消息队列微服务上一篇FanControl终极指南Windows风扇控制的专业解决方案下一篇麻雀AI助手终极指南3步打造你的智能麻将教练创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关推荐

蒙特卡罗模拟在工业工程中的应用:从产能瓶颈到投资决策
蒙特卡罗模拟在工业工程中的应用:从产能瓶颈到投资决策

还记得那次复盘会。车间主任把上个月的产量报表往桌上一拍,冲我们IE团队说:“按你们的测算,这条铆接线年产能100万件,怎么每个月都追料追到月底?”我们拿出的产能测算表确实白纸黑字:瓶颈工序节拍乘以稼动率… · 2026/9/24 22:19:18

ClassIsland桌面课程表:从部署到稳定运行的全流程实践
ClassIsland桌面课程表:从部署到稳定运行的全流程实践

讲个真实场景。我所在的学校前两年还在用最原始的方式管课表:每个教室前面贴一张打印纸,开学第一周刚贴上去还有学生看,两周后边角卷起来,上面被粉笔灰盖得看都看不清。学生问"下节什么课",答不上来的人比答… · 2026/9/24 22:19:18

网页色彩工程化:7种可落地的调色板实战指南
网页色彩工程化:7种可落地的调色板实战指南

1. 这不是配色理论课,是网页设计师每天要面对的“颜色决策现场”你刚打开Figma,页面上空荡荡的,客户说“想要年轻有活力但又不能太花哨”,产品经理甩来一句“主色调用品牌蓝,但按钮要让人一眼就想点”。这时候你盯着色… · 2026/9/24 22:19:12

ov2740 Linux驱动开发实战:从MIPI CSI-2到V4L2子设备驱动
ov2740 Linux驱动开发实战:从MIPI CSI-2到V4L2子设备驱动

简介:这份资源面向嵌入式Linux驱动开发者与摄像头模组调试人员,提供OV2740 CMOS图像传感器在Linux系统下的驱动源码,帮助解决传感器接入、配置与图像采集的底层实现问题。压缩包内共1个文件,为C语言源码,整体约7KB&… · 2026/9/24 23:03:14

Windows图标文件全解析:双图标之谜、SVN不显示与Word图标修复
Windows图标文件全解析:双图标之谜、SVN不显示与Word图标修复

每次帮人修电脑,总有人指着文件夹或者桌面问我:“为什么会有两个图标文件?一个正常显示,另一个要么是白板的,要么是乱码名字的,看着像病毒。”这个问题听起来小,但真去挖,背后是Wind… · 2026/9/24 23:03:14

UniRepLKNet实战:大核卷积图像分类与重参数化调优
UniRepLKNet实战:大核卷积图像分类与重参数化调优

简介:本资源面向图像分类方向的深度学习学习者与研究者,围绕大核卷积网络UniRepLKNet的实战应用展开,帮助读者理解大核ConvNets的架构设计思路,并探索卷积网络在视觉任务中的通用感知潜力。压缩包共2000个文件,以1990个… · 2026/9/24 23:03:07

Stable Diffusion+AnimateDiff可控视频生成实战指南
Stable Diffusion+AnimateDiff可控视频生成实战指南

1. 这不是“AI视频课”,而是一份可复现的生产流水线拆解你点开这个标题,大概率是被“百万播放”四个字钩住了——但我要先泼一盆常温水:没有算法黑箱、没有流量玄学、更没有所谓“AI自动爆火”的捷径。我带过37个零基础学员做AI视频&#xff… · 2026/9/24 23:03:07

UniRepLKNet大卷积核复兴:图像分类落地实战与避坑指南
UniRepLKNet大卷积核复兴:图像分类落地实战与避坑指南

简介:本资源面向图像分类方向的深度学习学习者与研究者,围绕大核卷积网络UniRepLKNet的实战应用展开,帮助读者理解大核ConvNets的架构设计思路,并验证卷积网络在视觉任务中的通用感知潜力。压缩包共2000个文件,以1990张… · 2026/9/24 23:03:07

LabVIEW+DAQmx数据采集系统实战:NI PCI-6221架构设计与稳定存储
LabVIEW+DAQmx数据采集系统实战:NI PCI-6221架构设计与稳定存储

做数据采集系统的同行应该都有过这种感觉:一开始拿到DAQ采集卡,觉得在LabVIEW里拖一个DAQmx读取节点,前面板能出波形就算大功告成。等真的要把数据稳定存下来、连续跑一整天不出幺蛾子、还要让别的系统能随时拿到数据的时候,才发现… · 2026/9/24 23:03:07

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13

1D-CNN时间序列建模实战:从Conv1d原理到工业落地
1D-CNN时间序列建模实战:从Conv1d原理到工业落地

简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26

柔软的L:汉语语流中被忽视的舌肌张力控制
柔软的L:汉语语流中被忽视的舌肌张力控制

1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码