一、前言

  • 强制解析(使用 !)是 Swift 语言中不可或缺的一个重要特点(特别是和 Objective-C 的接口混合使用时),它回避了一些其他问题,使得 Swift 语言变得更加优秀。
  • 比如在我的博客 Swift之深入解析如何处理非可选的可选项类型 中,在项目逻辑需要时使用强制解析去处理可选类型,将导致一些离奇的情况和崩溃。
  • 因此,尽可能地避免使用强制解析,将有助于搭建更加稳定的应用,并且在发生错误时提供更好的报错信息。那么如果是编写测试时,情况会怎么样呢?安全地处理可选类型和未知类型需要大量的代码,问题就在于我们是否愿意为编写测试做所有的额外工作,这就是我们需要探讨的问题。

二、测试代码 vs 产品代码

  • 当编写测试代码时,我们经常明确区分测试代码和产品代码,尽管保持这两部分代码的分离十分重要(我们不希望意外地让模拟测试对象成为 App Store 上架的部分),但就代码质量来说,没有必要进行明显区分。
  • 如果思考一下的话,想要对移交给使用者的代码进行高标准的要求,原因是什么呢?
    • 想要 App 为使用者稳定、流畅地运行;
    • 想要 App 在未来易于维护和修改;
    • 想要更容易让新人融入我们的团队。
  • 现在如果反过来考虑我们的测试,想要避免哪些事情呢?
    • 测试不稳定、脆弱、难于调试;
    • 当我们的 App 增加了新功能时,测试代码需要花费大量时间来维护和升级;
    • 测试代码对于加入团队的新人来说难于理解。
  • 之前很长的时间,我曾认为测试代码只是一些我快速堆砌的代码,因为有人告诉我必须要编写测试。我不那么在乎它们的质量,因为我将它视为一件琐事,并不将它放在首位。然而,一旦我因为编写测试而发现验证自己的代码有多么快,以及对自己有多么自信,我对测试的态度就开始了转变。
  • 所现在我相信对于测试代码,和将要移交的产品代码进行同等的高标准要求是非常重要的,因为我们配套的测试是需要长期使用、拓展和掌握的,理应让这些工作更容易完成。

三、强制解析的问题

  • 那么这一切与 Swift 中的强制解析有什么关系呢?有时必须要强制解析,很容易编写一个 “go-to solution” 的测试,来看一个例子,测试 UserService 实现的登陆机制是否正常工作:
class UserServiceTests: XCTestCase {func testLoggingIn() {// 为了登陆终端// 构建一个永远返回成功的模拟对象let networkManager = NetworkManagerMock()networkManager.mockResponse(forEndpoint: .login, with: ["name": "John","age": 30])// 构建 service 对象以及登录let service = UserService(networkManager: networkManager)service.login(withUsername: "john", password: "password")// 现在我们想要基于已登陆的用户进行断言,// 这是可选类型,所以我们对它进行强制解析let user = service.loggedInUser!XCTAssertEqual(user.name, "John")XCTAssertEqual(user.age, 30)}
}
  • 如你所见,在进行断言之前,我们强制解析了 service 对象的 loggedInUser 属性。像上面这样的做法并不是绝对意义上的错,但是如果这个测试因为一些原因开始失败,就可能会导致一些问题。
  • 假设某人(“某人”可能就是“未来的我们自己”)改变了网络部分的代码,导致上述测试开始崩溃。如果这样的事情发生了,错误信息可能只会像下面这样:
Fatal error: Unexpectedly found nil while unwrapping an Optional value
  • 尽管用 Xcode 本地运行时这不是个大问题(因为错误会被关联地显示,至少在大多数时候),但当连续地整体运行整个项目时,它可能问题重重。上述的错误信息可能出现在巨大的“文字墙”中,导致难以看出错误的来源。更严重的是,它会阻止后续的测试被执行(因为测试进程会崩溃),这将导致修复工作进展缓慢并且令人烦躁。

四、Guard 和 XCTFail

  • 一个潜在的解决上述问题的方式是简单地使用 guard 声明,优雅地解析问题中的可选类型,如果解析失败再调用 XCTFail 即可,就像下面这样:
guard let user = service.loggedInUser else {XCTFail("Expected a user to be logged in at this point")return
}
  • 尽管上述做法在某些情况下是正确的做法,但事实上我推荐避免使用它,因为它向测试中增加了控制流。为了稳定性和可预测性,通常希望测试只是简单的遵循 given,when,then 结构,并且增加控制流会使得测试代码难于理解。

五、保持可选类型

  • 另一个方法是让可选类型一直保持可选,这在某些使用情况下完全可用,包括 UserManager 的例子。因为对已经登录的 user 的 name 和 age 属性使用了断言,如果任意一个属性为 nil ,我们会自动得到错误提示。同时如果对 user 使用额外的 XCTAssertNotNil 检查,就能得到一个非常完整的诊断信息:
let user = service.loggedInUser
XCTAssertNotNil(user, "Expected a user to be logged in at this point")
XCTAssertEqual(user?.name, "John")
XCTAssertEqual(user?.age, 30)
  • 现在如果测试开始出错,就能得到如下信息:
XCTAssertNotNil failed - Expected a user to be logged in at this point
XCTAssertEqual failed: ("nil") is not equal to ("Optional("John")")
XCTAssertEqual failed: ("nil") is not equal to ("Optional(30)")
  • 这让我们能够更加容易地知道发生错误的地方,以及该从哪里入手去调试、解决这个错误。

六、使用 throw 的测试

  • 第三个选择在某些情况下是非常有用的,就是将返回可选类型的 API 替换为 throwing API。Swift 中的 throwing API 的优雅之处在于,需要时它能够非常容易地被当成可选类型使用。所以很多时候选择采用 throwing 方法,不需要牺牲任何的可用性。比如说,假设有一个 EndpointURLFactory 类,被用来在 App 中生成特定终端的 URL,这显然会返回可选类型:
class EndpointURLFactory {func makeURL(for endpoint: Endpoint) -> URL? {...}
}
  • 现在将其转换为采用 throwing API,像这样:
class EndpointURLFactory {func makeURL(for endpoint: Endpoint) throws -> URL {...}
}
  • 当我们仍然想得到一个可选类型的 URL 时,只需要使用 try? 命令去调用它:
let loginEndpoint = try? urlFactory.makeURL(for: .login)
  • 就测试而言,上述这种做法的最大好处在于可以在测试中轻松地使用 try,并且使用 XCTest runner 完全可以毫无代价地处理无效值。这是鲜为人知的,但事实上 Swift 测试可以是 throwing 函数,看看这个:
class EndpointURLFactoryTests: XCTestCase {func testSearchURLContainsQuery() throws {let factory = EndpointURLFactory()let query = "Swift"// 因为我们的测试函数是 throwing,这里我们可以简单地采用 'try'let url = try factory.makeURL(for: .search(query))XCTAssertTrue(url.absoluteString.contains(query))}
}
  • 没有可选类型,没有强制解析,某些发生错误的时候也能完美地做出诊断。

七、使用 require 的可选类型

  • 然而,并不是所有返回可选类型的 API 都可以被替换为 throwing,不过在写包含可选类型的测试时,有一个和 throwing API 同样好的方法。
  • 回到最开始 UserManager 的例子,如果既不对 loggedInUser 进行强制解析,又不把它看作可选类型,那么可以简单地这样做:
let user = try require(service.loggedInUser)
XCTAssertEqual(user.name, "John")
XCTAssertEqual(user.age, 30)
  • 这实在是太酷了,这样就可以摆脱大量的强制解析,同时避免让测试代码难于编写、难于上手。那么为了达到上述效果应该怎么做呢?这很简单,只需要对 XCTestCase 增加一个拓展,让我们分析任何可选类型表达式,并且返回非可选的值或者抛出一个错误,像这样:
extension XCTestCase {// 为了能够输出优雅的错误信息// 遵循 LocallizedErrowprivate struct RequireError<T>: LocalizedError {let file: StaticStringlet line: UInt// 实现这个属性非常重要// 否则测试失败时无法在记录中优雅地输出错误信息var errorDescription: String? {return "Required value of type \(T.self) was nil at line \(line) in file \(file)."}}// 使用 file 和 line 能够自动捕获// 源代码中出现的相对应的表达式func require<T>(_ expression: @autoclosure () -> T?,file: StaticString = #file,line: UInt = #line) throws -> T {guard let value = expression() else {throw RequireError<T>(file: file, line: line)}return value}
}
  • 现在有了上述内容,如果 UserManager 登录测试发生失败,也能得到一个非常优雅的错误信息,告诉我们错误发生的准确位置:
[UserServiceTests testLoggingIn] : failed: caught error: Required value of type User was nil at line 97 in file UserServiceTests.swift.
  • 它对所有可选类型增加了一个 require() 方法,以提高对无法避免的强制解析的诊断效果。请参考:Require。

八、总结

  • 以同样谨慎的态度对待应用代码和测试代码,在最开始可能有些不适应,但可以让长期维护测试变的更加简单,不论是独立开发还是团队开发,良好的错误诊断和错误信息是其中特别重要的一部分,使用本文中的一些技巧或许能够让你在未来避免很多奇怪的问题。
  • 我在测试代码中唯一使用强制解析的时候,就是在构建测试案例的属性时。因为这些总是在 setup 中被创建、tearDown 中被销毁,我并不把它们当作真正的可选类型。正如以往,你同样需要查看你自己的代码,根据你自己的喜好,来权衡决定。

九、参考资料

  • Handling non-optional optionals in Swift。

Swift之深入解析如何避免单元测试中的强制解析相关推荐

  1. python解析xml文件elementtree_Python中使用ElementTree解析XML示例

    [XML基本概念介绍] XML 指可扩展标记语言(eXtensible Markup Language). XML 被设计用来传输和存储数据. 概念一: 复制代码 代码如下: # foo元素的起始标签 ...

  2. Swift中关于可选类型(?)与强制解析(!)的特性

    2019独角兽企业重金招聘Python工程师标准>>> Swift中问号表示这是一个可选类型,白话翻译:某个常量或者变量可能是一个类型,也可能什么都没有,我不确定它是否真的会有值,也 ...

  3. iOS开发中的单元测试(三)——URLManager中的测试用例解析

    本文转载至 http://www.cocoachina.com/cms/plus/view.php?aid=8088   此前,我们在<iOS开发中的单元测试(一)&(二)>中介绍 ...

  4. 【全面解析Mock】Mock在单元测试中扮演一个什么角色?

    目录 一.Mock在单元测试中扮演一个什么角色 二.测试准备 三.使用Mock的理由 四.使用Python Mock 五.MagicMock类 六.mock.create_autospce 七.moc ...

  5. Git之深入解析如何在应用中嵌入Git

    一.前言 到目前为止,我们已经了解了 Git 基本的运作机制和使用方式,学习了许多 Git 提供的工具简单且有效地使用它,可以高效地帮助我们工作,提升我们的效率. 如果还不清楚 Git 的基础使用流程 ...

  6. Andorid中使用Jsoup解析库解析XML、HTML、Dom节点---第三方库学习笔记(三)

    XML介绍: XML简介: XML,可扩展标记语言,标准通用标记语言的子集. 一种用于标记电子文件使其具有结构性的标记语言. 它可以用来标记数据.定义数据类型 是一种允许用户对自己的标记语言进行定义的 ...

  7. 使用 Cobertura 和反射机制提高 Java 单元测试中的代码覆盖率

    本文将介绍两种开发实践,用于提高 Java 单元测试中的代码覆盖率.代码覆盖率 = (被测代码 / 代码总数)* 100%.提高被测代码数量或降低代码总数,均可达到提高代码覆盖率的效果.在本文中,您将 ...

  8. IBM技术论坛:使用 Cobertura 和反射机制提高单元测试中的代码覆盖率

    引言 单元测试是软件开发过程中重要的质量保证环节.单元测试可以减少代码中潜在的错误,使缺陷更早地被发现,从而降低了软件的维护成本.软件代码的质量由单元测试来保证,而单元测试自身的质量与效率问题也不容忽 ...

  9. Java中单元测试中:@BeforeClass,@Before,@Test,@After,@AfterClass中的问题详解

    在Junit4中还有的测试注解有:  @BeforeClass ,@Before,@Test,@After,@AfterClass 1.其中:@BeforeClass,@AfterClass是Juni ...

最新文章

  1. Unable to inject views for BcFragment{8d4c0 #1 id=0x7f0d00a1}
  2. python的常用数据类型_python 常用数据类型
  3. Intellij IDEA 默认打开上次项目设置与取消设置
  4. cuda卸载_Ubuntu18.04英伟达显卡驱动、Cuda安装
  5. GoF--服务定位器模式
  6. 河北省计算机网络技术专接本考什么,河北计算机专接本考什么
  7. SAP License:孔乙己,一名ERP顾问
  8. CF 55D Beautiful numbers 数位DP
  9. 【图像处理】图像强度变换、直方图均衡化(Image Intensity Transformations and Histogram Equalization)
  10. percona-toolkit源码编译安装
  11. 序列化和反序列化(五)——敏感字段加密
  12. Matlab APP Designer的基本使用过程以及技巧
  13. 透气清爽的高回弹跑鞋,跑步轻松畅快,咕咚逐日21K体验
  14. android上的壁纸软件,那些简约、精美、极致的安卓软件(APP) 篇四:这7个APP,满足你对壁纸所有的向往...
  15. Java 时间日期API总结
  16. dell服务器T100无法进入系统,戴尔电脑开机进不去,一直在转圈圈,怎么处理?
  17. 1.JAVA基础汇总
  18. 胡润首次发布《2019胡润全球独角兽榜》,11家区块链公司入选!
  19. java处理中文字符串_java中文字符串处理方法
  20. 全球与中国医疗3D扫描仪市场深度研究分析报告

热门文章

  1. RHEL4-VNC服务配置
  2. java EE map
  3. python实现网页登录时的rsa加密流程
  4. 07_UI基础_UITableView实战- 支付宝口碑
  5. object-c编程tips-timer
  6. 开源项目:单行日历(CalendarView)
  7. 配置错误:未能使用提供程序“RsaProtectedConfigurationProvider”进行解密。提供程序返回错误信息为: 打不开 RSA 密钥容器。...
  8. java:十进制转十六进制
  9. python小结_python简单小结
  10. java 检测ie版本更新_[Java教程]有关IE版本检测_星空网