当前位置: 首页 > 科技观察

Swift单元测试中避免强制解析

时间:2023-03-20 19:42:41 科技观察

本文转载自微信公众号《Snap开发》,作者Rickey王小吉。转载本文请联系网路发展公众号。前言强制解析(使用!)是Swift语言不可或缺的重要特性(尤其是与Objective-C接口混合使用时)。它回避了一些使Swift语言变得更好的其他问题。例如在Handlingnon-optionaloptionalvaluetypesinSwift[1]一文中,在项目逻辑需要时使用强制解析来处理可选类型会导致一些奇怪的情况和崩溃。因此,尽可能避免强制解析将有助于构建更稳定的应用程序,并在发生错误时提供更好的错误消息。那么在编写测试时会发生什么?安全地处理可选类型和未知类型需要大量代码,所以问题是我们是否愿意做所有额外的工作来编写测试。这就是我们本周要探索的内容,所以让我们开始吧!测试代码与生产代码在编写测试代码时,我们通常会明确区分测试代码和生产代码。虽然将这两段代码分开很重要(我们不想不小心让我们的模拟测试对象成为AppStore列表的一部分??),但就代码质量而言,没有必要做出明显的区分。你想一想,我们要对我们交给用户的代码有高标准,为什么?我们希望我们的应用程序能够为用户稳定流畅地运行。我们希望我们的应用程序将来易于维护和修改。我们想让新人更容易融入我们的团队。现在,如果我们反过来考虑我们的测试,我们想要避免的事情是什么?测试不稳定、脆弱且难以调试。当我们的应用程序添加新功能时,我们的测试代码需要花费大量时间来维护和升级。测试代码对于刚加入团队的新人来说很难看懂。你可能明白我在说什么??。很长一段时间,我认为测试代码只是我快速拼凑的一些代码,因为有人告诉我我必须编写测试。我不太关心它们的质量,因为我认为这是一件苦差事,没有把它放在首位。然而,一旦我发现我可以多快地验证我的代码,以及我因为编写测试而对自己有多么自信——我对测试的态度开始改变。所以现在我认为测试代码和将要交接的产品代码一样的高标准是非常重要的。因为我们的配套测试需要我们长期使用、扩展和掌握,所以我们应该让这些工作更容易完成。强制解析的问题那么这一切与Swift中的强制解析有什么关系???有时强制解析是必要的,并且很容易编写一个“go-tosolution”测试。下面看一个例子来测试UserService实现的登录机制是否正常工作:)networkManager.mockResponse(forEndpoint:.login,with:["name":"John","age":30])//构建服务对象并登录letservice=UserService(networkManager:networkManager)service.login(withUsername:"john",password:"password")//现在我们要根据登录用户进行断言,//这个是可选类型,所以强制解析letuser=service.loggedInUser!XCTAssertEqual(user.name,"John")XCTAssertEqual(user.age,30)}}如您所见,我们在进行断言之前强制解析服务对象的loggedInUser属性。像上面那样做并不是绝对错误的,但是如果测试由于某种原因开始失败,它可能会导致问题。假设某人(记住,“某人”可能是“你未来的自己”??)更改了网络部分的代码,导致上述测试开始崩溃。如果发生这样的事情,错误消息可能看起来像这样:Fatalerror:UnexpectedlyfoundnilwhileunwrappinganOptionalvalue虽然这在使用Xcode本机运行时不是一个大问题(因为错误将根据上下文显示-至少大部分时间??),将整个项目作为一个整体连续运行时可能会出现问题。上面的错误信息会出现在一个巨大的“文字墙”中,让人很难看出错误的来源。更糟糕的是,它阻止了后续测试的执行(因为测试过程崩溃),这使得修复工作缓慢且令人沮丧。Guard和XCTFail上述问题的潜在解决方案是简单地使用guard语句优雅地解析有问题的可选类型,如果解析失败则调用XCTFail,如下所示:guardletuser=service.loggedInUserelse{XCTFail("Expectedausertobeloggedinatthispoint")return尽管上述方法在某些情况下是正确的方法,但实际上我建议避免使用它——因为它会向您的测试添加控制流。为了稳定性和可预测性,您通常希望测试简单地遵循given,when,then结构,并且添加控制流会使测试代码难以理解。如果您真的不走运,控制流可能是误报的来源(在以后的帖子中会详细介绍)。另一种保持可选类型的方法是保持可选类型可选。这对于某些用例来说非常好,包括我们的UserManager示例。因为我们对登录用户的name和age属性使用了断言,如果这些属性中的任何一个为nil,我们会自动得到一个错误。此外,如果我们对用户使用额外的XCTAssertNotNil检查,我们可以获得非常完整的诊断。letuser=service.loggedInUserXCTAssertNotNil(user,"Expectedausertobeloggedinatthispoint")XCTAssertEqual(user?.name,"John")XCTAssertEqual(user?.age,30)现在如果我们的测试开始失败,我们可以得到以下信息::("nil")isnotequalto("Optional("John")")XCTAssertEqualfailed:("nil")isnotequalto("Optional(30)")这让我们更容易知道错误发生在哪里以及从哪里开始做什么从哪里开始调试和解决这个错误??。使用throw进行测试第三个选项在某些情况下非常有用,它是用throwingAPI替换返回可选类型的API。Swift中throwingAPI的美妙之处在于它可以在需要时轻松用作可选。所以在很多情况下,你会选择使用抛出的方式,而不会牺牲任何可用性。例如,假设我们有一个EndpointURLFactory类,用于为我们的应用程序中的特定端点生成URL,它显然返回一个可选类型:classEndpointURLFactory{funcmakeURL(forendpoint:Endpoint)->URL?{...}}现在我们转换它使用throwingAPI,像这样:command来调用它:letloginEndpoint=try?urlFactory.makeURL(for:.login)就测试而言,上面这种方式最大的好处就是在测试中可以很方便的使用try,而且可以不花钱的搞定使用XCTestrunner无效值。这个鲜为人知,但实际上Swift测试是可以抛出函数的,看这个:使用'try'leturl=tryfactory.makeURL(for:.search(query))XCTAssertTrue(url.absoluteString.contains(query))}}没有可选类型,没有强制解析,出现一些错误的时候也可以做一个完美诊断??。Optionalsusingrequire但是,并非所有返回可选值的API都可以通过抛出来替换。但是,有一种与throwingAPI一样好的方法可以编写包含可选类型的测试。让我们回到最初的UserManager示例。如果loggedInUser既不是强制性的也不是可选的,我们可以简单地做:letuser=tryrequire(service.loggedInUser)XCTAssertEqual(user.name,"John")XCTAssertEqual(user.age,30)这样我们就可以摆脱很多强制解析,同时避免让我们的测试代码难写难学。那么我们应该怎么做才能达到上面的效果呢?很简单,我们只需要给XCTestCase添加一个扩展,它允许我们分析任何可选类型表达式并返回一个非可选值或者抛出错误,像这样:extensionXCTestCase{//为了能够输出优雅的错误信息//我们遵循LocalizedErrowprivatestructRequireError:LocalizedError{letfile:StaticStringletline:UInt//这个属性的实现非常重要//否则我们无法在测试失败时优雅的在记录中输出错误信息varerrorDescription:String?{return"😱Requiredvalueoftype\(T.self)wasnilatline\(line)infile\(file)."}}//使用file和line可以让我们自动捕获//源代码中出现的对应表达式funcrequire(_expression:@autoclosure()->T?,file:StaticString=#file,line:UInt=#line)throws->T{guardletvalue=expression()else{throwRequireError(file:file,line:line)}returnvalue}}现在有了上面的内容,如果我们的UserManager登录测试失败了,我们也可以得到一个很e优雅的错误消息,告诉我们错误的确切位置。[UserServiceTeststestLoggingIn]:failed:caughterror:😱RequiredvalueoftypeUserwasnilatline97infileUserServiceTests.swift。您可能意识到这个技巧来自我的迷你框架Require[2],它向所有可选类型添加了一个require()方法,以改进对强制解析的不可避免的诊断效果的支持。总结像对待测试代码一样谨慎对待您的应用程序代码一开始可能会让人不舒服,但它可以使长期维护测试变得容易得多—无论您是独立开发还是作为团队开发。良好的错误诊断和错误消息是其中一个特别重要的部分,使用本文中的一些技巧可能会避免您将来遇到很多奇怪的问题。我唯一一次在测试代码中使用强制解析是在构建测试用例属性时。因为它们总是在setUp中创建并在tearDown中销毁,所以我不认为它们是真正的可选类型。与往常一样,您还需要查看自己的代码并根据自己的喜好权衡决定。所以你怎么看?您会采用本文中的某些技术,还是使用过相关的技术?请让我知道,包括您可能有的任何问题、评论和反馈。