加入收藏 | 设为首页 | 会员中心 | 我要投稿 厦门网 (https://www.xiamenwang.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程 > 正文

避免 Swift 单元测试中的强制分析

发布时间:2021-11-07 11:15:15 所属栏目:编程 来源:互联网
导读:前言 强制解析(使用 !)是 Swift 语言中不可或缺的一个重要特点(特别是和 Objective-C 的接口混合使用时)。它回避了一些其他问题,使得 Swift 语言变得更加优秀。比如 处理 Swift 中非可选的可选值类型[1] 这篇文章中,在项目逻辑需要时使用强制解析去处理可选
前言
强制解析(使用 !)是 Swift 语言中不可或缺的一个重要特点(特别是和 Objective-C 的接口混合使用时)。它回避了一些其他问题,使得 Swift 语言变得更加优秀。比如 处理 Swift 中非可选的可选值类型[1] 这篇文章中,在项目逻辑需要时使用强制解析去处理可选类型,将导致一些离奇的情况和崩溃。
 
所以尽可能地避免使用强制解析,将有助于搭建更加稳定的应用,并且在发生错误时提供更好的报错信息。那么如果是编写测试时,情况会怎么样呢?安全地处理可选类型和未知类型需要大量的代码,那么问题就在于我们是否愿意为编写测试做所有的额外工作。这就是我们这周将要探讨的问题,让我们开始深入研究吧!
 
测试代码 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)")
这让我们能够更加容易地知道发生错误的地方,以及该从哪里入手去调试、解决这个错误。

(编辑:厦门网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    热点阅读