1. 项目概述:当iOS测试遇上Cucumber
如果你是一名iOS开发者或测试工程师,面对一个功能模块复杂、业务逻辑交织的应用,是否曾为编写和维护那些动辄上千行的UI自动化测试脚本而头疼?脚本与业务描述脱节,产品经理看不懂,测试同学改起来也小心翼翼。这正是我几年前在负责一个大型金融类iOS应用时遇到的真实困境。直到我们引入了Cucumber,将自动化测试从“代码驱动”转变为“场景驱动”,整个团队的协作效率和测试用例的可读性才发生了质的变化。今天,我就以“Frank自动化测试实战”为引子,深入拆解如何基于Cucumber为iOS应用构建一套健壮、可维护的场景化自动化测试框架。这不是一个简单的工具使用教程,而是一套融合了工程实践、团队协作和避坑经验的完整解决方案。
简单来说,这个项目的核心是:用人类看得懂的自然语言(主要是Gherkin语法)来描述iOS应用的业务场景和行为,然后通过自动化代码(通常是Swift或Objective-C)去执行这些描述,实现从需求文档到自动化测试用例的无缝衔接。它解决的不仅是“自动化”的问题,更是“沟通”和“文档化”的问题。适合阅读这篇内容的你,可能是正在寻求提升测试代码质量的iOS开发者,也可能是希望测试脚本更贴近业务、便于维护的QA工程师,或者是想了解如何落地BDD(行为驱动开发)的团队技术负责人。
2. 核心设计:为什么是Cucumber + iOS?
在iOS自动化测试领域,我们有XCTest、Appium、EarlGrey等多种选择。单独来看,XCTest与Xcode集成度最高,性能最好;Appium跨平台能力强;EarlGrey来自Google,断言能力强大。但当我们把可读性、可维护性和团队协作作为更高优先级的目标时,Cucumber的优势就凸显出来了。
2.1 BDD理念与Gherkin语法的价值
Cucumber是BDD理念的实践工具。BDD不是一种测试技术,而是一种软件开发方法,它鼓励开发者、测试者、业务分析师等所有项目成员使用统一的语言来讨论软件的行为。这个统一的语言就是Gherkin。
一个典型的Gherkin场景(Feature文件)看起来是这样的:
功能:用户登录 为了确保账户安全 作为一名注册用户 我希望能够使用正确的凭据登录应用 场景大纲:使用有效凭据登录成功 假设 用户已注册且账号未被锁定 当 用户在“手机号”输入框输入“<手机号>” 并且 用户在“密码”输入框输入“<密码>” 并且 用户点击“登录”按钮 那么 用户应跳转至首页 并且 首页应显示用户昵称“<昵称>” 例子: | 手机号 | 密码 | 昵称 | | 13800138000 | Pass123! | 张三 | | 13900139000 | Test456@ | 李四 |这种写法的魔力在于:
- 非技术人员能看懂:产品经理、业务方可以轻松阅读并确认这些场景是否准确描述了需求,甚至可以直接参与编写。
- 活文档:这些
.feature文件本身就是最新、可执行的业务需求文档。需求变更时,首先修改这里,测试失败会立刻提醒你文档与实现不同步。 - 降低维护成本:当登录页面的UI元素ID改变时,你只需要在一个地方(步骤定义代码)修改定位逻辑,所有引用该步骤的场景都自动生效,无需在成百上千个测试脚本中搜索替换。
2.2 技术栈选型:Cucumberish与Frank
在iOS原生生态中,要让Cucumber运行起来,我们需要一个“桥梁”库,它负责解析.feature文件,并将其映射到我们编写的原生代码(步骤定义)。主流选择有两个:Cucumberish和Frank。
- Cucumberish:一个纯Objective-C的Cucumber实现,轻量级,配置简单。它通过运行时(Runtime)动态地将Feature文件中的步骤与你的OC方法关联起来。对于Swift项目,需要通过桥接文件来使用。
- Frank:一个更老牌、功能更丰富的iOS验收测试框架。它本身包含了一个应用驱动(App Driver)、Cucumber集成以及一些工具链。Frank更像一个“全家桶”,但配置相对复杂,社区活跃度已不如前。
在我们的实战项目中,我选择了Cucumberish。原因如下:
- 轻量且活跃:Cucumberish专注于做好Cucumber的iOS适配,不捆绑其他复杂组件,与现有的XCTest单元测试框架融合得更好。其GitHub社区相对活跃,Issue响应较快。
- 对Swift支持尚可:虽然底层是OC,但通过
@objc暴露Swift方法,使用起来没有太大障碍。这对于现代以Swift为主的iOS项目是可以接受的。 - 调试友好:测试执行完全在Xcode中,可以利用LLDB进行断点调试,这对于排查复杂的交互逻辑或异步问题至关重要。而一些基于远程WebDriver协议(如Appium)的方案,调试体验会打折扣。
当然,这个选择并非绝对。如果你的项目历史包袱重,全是OC代码,或者需要一些Frank提供的额外工具(如符号化命令行工具),Frank也是一个可选项。但就目前社区的流行度和上手简易度而言,Cucumberish是大多数团队更稳妥的起点。
注意:无论选择哪个,都需要在项目中引入对应的依赖。Cucumberish通常通过CocoaPods或Swift Package Manager集成。确保你的iOS部署目标(Deployment Target)版本与其兼容。
3. 环境搭建与项目初始化
理论说再多,不如动手搭一遍。下面我将以一个新项目为例,演示如何从零搭建一个基于Cucumberish的iOS UI自动化测试工程。假设我们的主工程是一个名为MyAwesomeApp的SwiftUI应用。
3.1 创建独立的UI测试Target
我强烈建议将Cucumber自动化测试放在一个独立的UI测试Target中,而不是与单元测试混在一起。这样做的好处是依赖清晰、构建目标明确,且不会污染主应用的发布配置。
- 在Xcode中,打开你的主工程
MyAwesomeApp.xcodeproj。 - 点击菜单栏
File->New->Target...。 - 在模板选择器中,选择
iOS->Test->UI Testing Bundle。点击Next。 - 输入产品名称,例如
MyAwesomeAppUITests。确保Project和Embed in Application都正确选择了你的主应用。点击Finish。
现在,你的项目导航器中会多出一个MyAwesomeAppUITests目录,里面包含一个MyAwesomeAppUITests.swift文件。这个Target将是我们所有Cucumber测试代码的家。
3.2 集成Cucumberish依赖
我们使用Swift Package Manager (SPM)来添加Cucumberish,这是目前最推荐的方式,无需管理复杂的CocoaPods依赖链。
- 在Xcode中,点击项目导航器顶部的项目文件,选中
MyAwesomeApp项目。 - 选择
MyAwesomeAppUITestsTarget,然后切换到Package Dependencies标签页。 - 点击
+按钮,在搜索框中输入https://github.com/Ahmed-Ali/Cucumberish。 - Xcode会找到Cucumberish仓库。在
Dependency Rule处,通常选择Up to Next Major Version,并指定一个版本范围,例如1.2.0。点击Add Package。 - 在下一个弹窗中,务必确保只勾选
MyAwesomeAppUITests这个Target。不要勾选主应用Target或其他Target。点击Add Package。
等待Xcode解析并下载依赖完成。完成后,你可以在MyAwesomeAppUITestsTarget的Frameworks and Libraries中看到Cucumberish。
3.3 配置Cucumberish启动入口
Cucumberish需要一个启动配置来加载Feature文件和步骤定义。我们需要修改UI测试的入口文件。
- 删除自动生成的
MyAwesomeAppUITests.swift文件。 - 新建一个Swift文件,命名为
CucumberishHooks.swift。这个文件将包含我们的配置代码。 - 在
CucumberishHooks.swift中,写入以下代码:
import XCTest import Cucumberish class CucumberishHooks: NSObject { @objc class func setupCucumber() { // 1. 告诉Cucumberish在哪里寻找Feature文件 let bundle = Bundle(for: CucumberishHooks.self) // 假设我们把.feature文件放在UITests target的`Features`文件夹下 let featureFilePaths = bundle.paths(forResourcesOfType: “.feature”, inDirectory: nil) // 2. 定义一个闭包,用于在每个Scenario开始前执行(例如:启动App) let beforeStart: (CCIScenarioDefinition?) -> Void = { _ in // 这里可以启动你的App。对于UI测试,通常XCTestCase的setUp方法会处理。 // 但我们可以在这里执行一些全局前置条件,比如重置App状态。 // 注意:UI测试中启动App通常由XCTestCase管理,这里更多是逻辑准备。 print(“即将开始运行一个Scenario...”) } // 3. 配置Cucumberish Cucumberish.executeFeatures( atPaths: featureFilePaths, from: bundle, includeTags: nil, // 可以指定只运行包含某些tag的Scenario,如 [“@smoke”] excludeTags: nil, // 可以指定排除某些tag的Scenario beforeStart: beforeStart ) } }- 我们需要一个机制在UI测试启动时调用
setupCucumber()。由于UI测试本身是XCTestCase的子类,我们可以利用Test Observer。但更简单的方法是修改MyAwesomeAppUITeststarget的Info.plist。 - 找到
MyAwesomeAppUITests目录下的Info.plist文件,用源代码方式打开(右键 -> Open As -> Source Code)。 - 找到
<dict>标签内的部分,添加或修改以下键值对:
<key>CFBundlePrincipalClass</key> <string>$(PRODUCT_MODULE_NAME).CucumberishHooks</string>这个设置告诉XCTest,将CucumberishHooks类作为测试Bundle的主要入口点。
3.4 编写第一个Feature文件与步骤定义
框架搭好了,我们来创建第一个可运行的测试场景。
- 在
MyAwesomeAppUITests组下,新建一个文件夹(Group),命名为Features。注意:是Group(黄色文件夹图标),不是物理文件夹(蓝色文件夹图标)。这样Xcode会将其作为资源包含进Bundle。 - 在
Features文件夹内,右键 ->New File...,选择Other->Empty,创建一个空文件,命名为login.feature。 - 在
login.feature中,写入我们之前举例的Gherkin场景。 - 现在,我们需要为这些自然语言步骤编写实际的自动化代码,即“步骤定义”(Step Definitions)。
- 在
MyAwesomeAppUITests组下,新建一个Swift文件,命名为LoginSteps.swift。 - 在
LoginSteps.swift中,我们需要用Cucumberish的宏来将Gherkin步骤映射到Swift函数:
import XCTest import Cucumberish class LoginSteps: NSObject { // 我们需要一个XCTestCase实例来访问application和进行UI交互 // 通常我们会用一个共享的上下文来传递。这里为了简单,我们用一个静态变量。 // 更优雅的做法是使用Cucumberish的“世界”对象或依赖注入,但初期可以这样。 static var currentTestCase: XCTestCase? static var app: XCUIApplication? @objc class func setup() { // 注册 “假设 用户已注册且账号未被锁定” Given(“^用户已注册且账号未被锁定$”) { _, _ in // 这个步骤是前置条件,可能不需要具体操作,或者需要模拟网络状态。 // 例如,在测试开始前,调用一个Mock API来确保测试账号状态正常。 // 这里我们只打印日志。 print(“[前提] 测试账号状态正常”) } // 注册 “当 用户在“手机号”输入框输入“<手机号>”” // 注意正则表达式中的捕获组 (.*?) 用于匹配例子中的变量 When(“^用户在“手机号”输入框输入“(.*?)”$”) { args, _ in let phoneNumber = args![0] // 获取第一个捕获组的值 // 找到手机号输入框并输入 let phoneField = LoginSteps.app!.textFields[“手机号输入框”] // 使用可访问性标识符 XCTAssertTrue(phoneField.waitForExistence(timeout: 5), “手机号输入框未找到”) phoneField.tap() phoneField.typeText(phoneNumber) } // 注册 “并且 用户在“密码”输入框输入“<密码>”” And(“^用户在“密码”输入框输入“(.*?)”$”) { args, _ in let password = args![0] let passwordField = LoginSteps.app!.secureTextFields[“密码输入框”] // 密码输入框通常是Secure XCTAssertTrue(passwordField.waitForExistence(timeout: 2), “密码输入框未找到”) passwordField.tap() passwordField.typeText(password) } // 注册 “并且 用户点击“登录”按钮” And(“^用户点击“登录”按钮$”) { _, _ in let loginButton = LoginSteps.app!.buttons[“登录按钮”] XCTAssertTrue(loginButton.waitForExistence(timeout: 2), “登录按钮未找到”) loginButton.tap() } // 注册 “那么 用户应跳转至首页” Then(“^用户应跳转至首页$”) { _, _ in // 如何断言跳转到首页?可以通过检查首页特有的元素是否存在 let homeTabBar = LoginSteps.app!.tabBars.buttons[“首页”] // 等待并断言该元素存在,并且可能是被选中的状态(如果是TabBar) XCTAssertTrue(homeTabBar.waitForExistence(timeout: 10), “登录后未成功跳转到首页”) } // 注册 “并且 首页应显示用户昵称“<昵称>”” And(“^首页应显示用户昵称“(.*?)”$”) { args, _ in let expectedNickname = args![0] let nicknameLabel = LoginSteps.app!.staticTexts[expectedNickname] // 或者通过其他标识符查找 // 注意:这里直接用文本查找可能不稳定,最好使用固定的accessibilityIdentifier // 例如:let nicknameLabel = LoginSteps.app!.staticTexts[“userNicknameLabel”] // 然后断言其label等于expectedNickname XCTAssertTrue(nicknameLabel.waitForExistence(timeout: 5), “未找到显示昵称的标签”) XCTAssertEqual(nicknameLabel.label, expectedNickname) } } }- 最后,我们需要在
CucumberishHooks.setupCucumber()方法中,在调用executeFeatures之前,注册我们的步骤定义类,并初始化App。
// 在CucumberishHooks.swift的setupCucumber方法中,beforeStart闭包之前添加: // 初始化步骤定义 LoginSteps.setup() // 在beforeStart闭包中或之前,启动App let app = XCUIApplication() LoginSteps.app = app // 通常我们会在每个Scenario开始时启动App,这里先启动一次。 app.launch() // 你也可以选择在before闭包中启动,确保每个Scenario都是干净状态。实操心得:为UI元素设置稳定的
accessibilityIdentifier是UI自动化的基石。不要在步骤定义里用不稳定的文本或坐标定位。和开发团队约定,为关键交互元素添加accessibilityIdentifier,这不仅能用于测试,也对无障碍功能有益。例如,在SwiftUI中,可以这样设置:TextField(“手机号”, text: $phone).accessibilityIdentifier(“手机号输入框”)。
4. 核心环节实现:构建可维护的测试架构
上面的例子跑通了一个简单的登录场景。但对于一个真实项目,我们需要更健壮、更易维护的架构。直接把所有步骤定义和交互代码堆在一个文件里,很快就会变成“屎山”。下面分享我们实战中总结出的分层架构。
4.1 三层架构:Feature -> Steps -> PageObject
1. Feature层 (Gherkin场景)
- 职责:纯业务描述,不涉及任何技术细节。由产品、测试、开发共同维护。
- 组织方式:按功能模块分目录。例如:
Features/Account/Login.feature,Features/Account/Profile.feature,Features/Payment/Checkout.feature。 - 技巧:善用
@tag。例如,给核心冒烟测试场景打上@smoke,给耗时长的场景打上@slow。在Cucumberish执行时,可以通过includeTags和excludeTags参数灵活选择要运行的场景集。
2. Steps层 (步骤定义)
- 职责:将Gherkin步骤翻译成对PageObject的方法调用。它应该很“薄”,只做流程编排和数据传递,不包含具体的UI查找和操作逻辑。
- 组织方式:与Feature目录结构对应。例如:
StepDefinitions/Account/LoginSteps.swift,StepDefinitions/Payment/CheckoutSteps.swift。 - 优化后的LoginSteps示例:
// LoginSteps.swift import Cucumberish class LoginSteps: NSObject { @objc class func setup() { var loginPage: LoginPageObject? Given(“^用户已注册且账号未被锁定$”) { _, _ in // 可能不需要操作,或重置测试环境 TestHelper.resetTestAccount() } When(“^用户在“手机号”输入框输入“(.*?)”$”) { args, _ in loginPage = LoginPageObject() // 惰性初始化也可 loginPage!.enterPhoneNumber(args![0]) } And(“^用户在“密码”输入框输入“(.*?)”$”) { args, _ in loginPage!.enterPassword(args![0]) } And(“^用户点击“登录”按钮$”) { _, _ in loginPage!.tapLoginButton() } Then(“^用户应跳转至首页$”) { _, _ in let homePage = HomePageObject() XCTAssertTrue(homePage.isActive(), “登录后未进入首页”) } And(“^首页应显示用户昵称“(.*?)”$”) { args, _ in let homePage = HomePageObject() let actualNickname = homePage.getUserNickname() XCTAssertEqual(actualNickname, args![0]) } } }可以看到,步骤定义里已经没有XCUIApplication().textFields[...]这样的代码了,所有UI细节被封装到了PageObject中。
3. PageObject层 (页面对象)
- 职责:封装单个页面或组件的所有UI元素定位和基本操作。这是最核心的、与UI直接交互的一层。
- 组织方式:按页面划分。例如:
PageObjects/LoginPageObject.swift,PageObjects/HomePageObject.swift。 - LoginPageObject示例:
// LoginPageObject.swift import XCTest class LoginPageObject { private let app: XCUIApplication init(app: XCUIApplication = XCUIApplication()) { self.app = app } // UI元素封装为计算属性 private var phoneNumberField: XCUIElement { return app.textFields[“login_phone_field”] // 使用accessibilityIdentifier } private var passwordField: XCUIElement { return app.secureTextFields[“login_password_field”] } private var loginButton: XCUIElement { return app.buttons[“login_submit_button”] } // 页面操作封装为方法 func enterPhoneNumber(_ number: String) { XCTAssertTrue(phoneNumberField.waitForExistence(timeout: 5), “手机号输入框未找到”) phoneNumberField.tap() // 先清空再输入,避免旧数据干扰 if let currentValue = phoneNumberField.value as? String, !currentValue.isEmpty { phoneNumberField.clearText() // 需要扩展XCUIElement来实现clearText } phoneNumberField.typeText(number) } func enterPassword(_ password: String) { XCTAssertTrue(passwordField.waitForExistence(timeout: 2), “密码输入框未找到”) passwordField.tap() passwordField.typeText(password) } func tapLoginButton() { XCTAssertTrue(loginButton.waitForExistence(timeout: 2), “登录按钮未找到”) loginButton.tap() } // 页面状态断言 func isLoginPageVisible() -> Bool { return phoneNumberField.exists && loginButton.exists } } // XCUIElement扩展,用于清空文本 extension XCUIElement { func clearText() { guard let stringValue = self.value as? String else { return } let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: stringValue.count) self.typeText(deleteString) } }这种分层架构的好处是巨大的:
- 高可维护性:当登录页面的UI改版,输入框的ID变了,你只需要修改
LoginPageObject中的一个属性,所有用到这个输入框的测试场景(可能分布在几十个Feature文件中)都自动生效。 - 高可读性:步骤定义读起来就像是在讲业务故事,而具体的操作细节被隐藏在了PageObject里。
- 便于协作:测试同学可以专注于编写和维护Feature文件和步骤定义,而开发同学可以帮助封装和维护稳定、高效的PageObject。
4.2 数据驱动与场景大纲
Gherkin的场景大纲配合例子表格,是数据驱动测试的利器。它允许你用同一套步骤逻辑,测试多组输入数据和预期结果。这在测试边界值、等价类时特别有用。
场景大纲:登录功能输入验证 当 用户在“手机号”输入框输入“<手机号>” 并且 用户在“密码”输入框输入“<密码>” 并且 用户点击“登录”按钮 那么 页面应显示提示信息“<预期提示>” 例子: | 手机号 | 密码 | 预期提示 | | | 123456 | 手机号不能为空 | | 1380013800 | 123456 | 手机号格式不正确 | | 13800138000 | | 密码不能为空 | | 13800138000 | 123 | 密码长度至少6位 |在步骤定义中,你可以通过args![0],args![1]来获取例子表中每一列的值。这极大地减少了重复的Scenario编写。
4.3 钩子(Hooks)的使用
Cucumberish支持钩子,允许你在特定阶段执行代码,比如在每个Scenario之前或之后。这对于环境准备和清理工作至关重要。
// 在CucumberishHooks.swift或专门的Hooks文件中 import Cucumberish class Hooks: NSObject { @objc class func setup() { // 在每个Scenario开始前执行(在beforeStart闭包之后,具体步骤之前) Before({ _ in print(“——— 开始执行一个新的Scenario ———”) // 确保App处于前台,或者重启App以获得干净状态 let app = XCUIApplication() if app.state != .runningForeground { app.terminate() app.launch() } // 重置用户偏好设置、清理钥匙链等 TestHelper.clearKeychain() TestHelper.resetUserDefaults() }) // 在每个Scenario结束后执行(无论成功失败) After({ _ in print(“——— Scenario执行结束 ———”) // 截图(如果失败) if let currentTestCase = XCTestCase.current { let screenshot = XCUIScreen.main.screenshot() let attachment = XCTAttachment(screenshot: screenshot) attachment.lifetime = .keepAlways currentTestCase.add(attachment) } // 可能还需要一些清理工作 }) // 还可以围绕每个Step设置钩子,但通常Scenario级别的钩子已足够。 } }记得在CucumberishHooks.setupCucumber()中调用Hooks.setup()。
5. 常见问题与排查技巧实录
在实际项目中落地Cucumber for iOS,我踩过不少坑。下面把这些“血泪教训”整理成排查清单,希望能帮你节省大量时间。
5.1 问题:Cucumberish测试不执行,控制台无输出
- 可能原因1:Feature文件没有被正确复制到测试Bundle中。
- 排查:检查
MyAwesomeAppUITeststarget的Build Phases->Copy Bundle Resources。确保你的Features文件夹或.feature文件在其中。如果使用Group,通常会自动添加。也可以检查编译后的App包内容,看.feature文件是否存在。
- 排查:检查
- 可能原因2:
Info.plist中的CFBundlePrincipalClass设置错误。- 排查:确认
$(PRODUCT_MODULE_NAME)展开后是否正确指向了CucumberishHooks类所在的模块。一个保险的做法是写死模块名,如MyAwesomeAppUITests.CucumberishHooks。
- 排查:确认
- 可能原因3:步骤定义没有正确注册。
- 排查:确保在
Cucumberish.executeFeatures调用之前,执行了所有Steps类的setup方法。添加一些打印日志来确认。
- 排查:确保在
5.2 问题:步骤定义匹配失败(Step undefined)
- 可能原因1:正则表达式不匹配。
- 排查:Cucumberish的步骤匹配是严格基于正则表达式的。注意中英文符号、空格。例如,
当 用户点击“登录”按钮和当用户点击“登录”按钮(缺少空格)是无法匹配的。建议复制Feature文件中的步骤文本到正则表达式中。 - 技巧:在步骤定义函数里加一行打印,确认函数被调用。
- 排查:Cucumberish的步骤匹配是严格基于正则表达式的。注意中英文符号、空格。例如,
- 可能原因2:步骤定义类没有被正确初始化或链接。
- 排查:确保步骤定义类(如
LoginSteps)是NSObject的子类,并且注册方法setup是@objc class func。如果项目是纯Swift,确保包含步骤定义的Swift文件被包含在UITests target的编译源中。
- 排查:确保步骤定义类(如
5.3 问题:UI元素找不到(No matches found)
这是UI自动化中最常见的问题。
- 可能原因1:
accessibilityIdentifier设置错误或未设置。- 排查:使用Xcode的
Debug View Hierarchy工具,在App运行时暂停,检查目标元素是否有accessibilityIdentifier,值是否正确。永远不要依赖不稳定的文本或坐标。
- 排查:使用Xcode的
- 可能原因2:元素尚未加载出来。
- 排查:在PageObject中使用
waitForExistence(timeout:)而不是直接判断exists。给足合理的超时时间(如5-10秒)。对于网络加载慢的页面尤其重要。
- 排查:在PageObject中使用
- 可能原因3:元素在嵌套的视图或WebView中。
- 排查:XCUIApplication的查询是全局的,但有时需要更精确的路径。例如,
app.webViews.staticTexts[“某个文本”]。使用po app.descendants(matching: .any)在控制台打印所有元素树,辅助定位。
- 排查:XCUIApplication的查询是全局的,但有时需要更精确的路径。例如,
- 可能原因4:App有多个Window或Alert。
- 排查:注意
XCUIApplication()获取的是当前激活的App实例。如果有系统弹窗(如通知、定位权限),可能需要先处理它们。可以使用app.alerts来查询并操作Alert。
- 排查:注意
5.4 问题:测试执行速度慢
- 优化1:合理使用
launchArguments和launchEnvironment。- 在App启动时传入特定参数,让App进入测试模式。例如,关闭动画、禁用新手引导、使用Mock网络数据。
在主App代码中,可以读取这些环境变量来调整行为。let app = XCUIApplication() app.launchArguments.append(“-UITest”) app.launchEnvironment[“MOCK_NETWORK”] = “YES” app.launch() - 优化2:避免不必要的App重启。
- 在每个Scenario前重启App固然干净,但极其耗时。评估你的测试场景是否真的需要完全干净的状态。很多场景可以共享登录态,使用
@Before钩子进行部分重置(如清除特定数据)而非完全重启。
- 在每个Scenario前重启App固然干净,但极其耗时。评估你的测试场景是否真的需要完全干净的状态。很多场景可以共享登录态,使用
- 优化3:使用标签过滤。
- 给快速的核心场景打上
@fast或@smoke标签。日常开发中只运行这些标签的测试。全量测试可以在CI/CD流水线中夜间执行。
- 给快速的核心场景打上
5.5 问题:异步操作导致断言失败
- 解决方案:使用
XCTest的异步期望(XCTestExpectation)或XCUIElement的waitFor系列方法。 - PageObject中的最佳实践:将等待逻辑封装在PageObject的方法内部。
func waitForHomePageLoad(timeout: TimeInterval = 10) -> Bool { let homeIndicator = app.otherElements[“home_view”] // 首页的一个独特元素 return homeIndicator.waitForExistence(timeout: timeout) } // 在步骤定义或PageObject的其他方法中调用 XCTAssertTrue(homePage.waitForHomePageLoad(), “首页加载超时”)5.6 问题:测试报告不直观
- 解决方案:集成第三方报告生成器。Cucumberish本身输出是控制台文本。可以集成
CucumberishReporting或使用xchtmlreport等工具来生成更美观的HTML报告。也可以在After钩子中,将结果和截图整合,上传到团队内部的测试管理平台。
6. 持续集成与团队协作
自动化测试的价值在持续集成(CI)中才能最大化体现。我们使用Jenkins和Fastlane来搭建流水线。
- Fastlane配置:创建一个
Fastfile,定义测试lane。
# fastlane/Fastfile default_platform(:ios) platform :ios do desc “运行Cucumber UI测试” lane :run_ui_tests do # 1. 构建用于测试的App和UITests bundle build_app( scheme: “MyAwesomeApp”, configuration: “Debug” ) # 2. 运行指定的UI测试 run_tests( scheme: “MyAwesomeAppUITests”, devices: [“iPhone 15”], # 指定测试设备 code_coverage: true, # 生成代码覆盖率报告 output_directory: “./test_output”, output_types: “html,junit”, derived_data_path: “./DerivedData” ) # 3. 可选:处理测试报告,如发送通知、上传结果 slack( message: “UI测试运行完成!” ) end endJenkins Pipeline:在Jenkins中创建一个Pipeline项目,拉取代码后,执行
bundle exec fastlane run_ui_tests。可以将测试结果(JUnit格式)与Jenkins插件集成,在构建页面展示历史和趋势。团队协作流程:
- 需求阶段:产品经理(或BA)在Jira等工具中编写用户故事(User Story)。
- 开发启动前:开发、测试、产品三方进行“三 amigos”会议,基于用户故事共同编写Gherkin场景(.feature文件),形成验收标准。这个文件放入代码库。
- 开发过程中:开发者实现功能,并同时实现对应的步骤定义和PageObject(或由测试工程师完成)。测试驱动开发(TDD)的升级版——行为驱动开发(BDD)就此实践。
- 提测与合并:功能开发完成,对应的Cucumber测试也应当通过。将包含.feature文件和实现代码的PR合并到主分支。CI会自动运行测试,确保新功能不破坏现有场景。
这套流程将测试从“事后验证”变成了“事前约定”和“事中保障”,极大地提升了交付质量和团队效率。
从我个人的实战经验来看,引入Cucumber和BDD最大的挑战往往不是技术,而是团队思维和协作习惯的转变。初期可能会觉得写Feature文件多此一举,但一旦团队适应了这种“用同一种语言说话”的方式,其带来的沟通成本降低和需求理解一致性的提升,价值远超工具本身。从“测试脚本”到“可执行的需求文档”,这才是自动化测试走向成熟的关键一步。