如何在Swift中测试抛出代码


您必须接管多少次进行了单元测试的项目,但是这些项目很难掌握,拼命失败,或者甚至无法建立测试目标?

保持单元测试代码的健壮性和可维护性,而不是让它们随着时间的流逝而被遗弃和忽略,至关重要。

在Storytel,我们尝试使单元测试简短易读。 由于测试代码的性质,代码往往会增长很长且具有重复性,因此保持代码整洁很重要,然后随着项目的增长,进行测试的工作就不会变得太乏味。

有时,抛出的代码可能很难测试,并且最终的测试代码可能很难看。 在本文中,我将深入探讨不同的场景,以及如何以出色而强大的方式解决它们。


XCTest是一个功能强大的框架。 在Xcode 8.3中,Apple引入了几个新的XCTAssert函数,除了几个现有的函数。 尽管它们提供的功能允许执行开发人员希望执行的大多数操作,但某些操作仍然需要在本机提供的功能之上的样板代码。

让我们看看一些案例以及我们如何解决它们。

比较抛出函数的结果

这很容易。 现有的所有XCTAssert函数都已经带有抛出参数。 如果结果为Equatable ,则将执行以下操作:

  XCTAssertEqual(尝试x.calculateValue(),ExpectedValue) 

如果calculateValue()引发错误,则测试失败,并显示消息“ XCTAssertEqual失败:引发错误…… ”。 如果调用未引发并且两个值不相等,则测试失败并显示消息“ XCTAssertEqual failed:a不等于b ”,其中“ a ”和“ b ”分别是由String(describing:)
简而言之,所有XCTAssert *值检查函数都会验证调用不会引发任何错误
返回预期结果。 很方便。

具有非等于返回类型的函数

通常XCTAssertEqual及其值检查同级是不够的。

如果函数不返回值,或者在测试用例的上下文中可以将其忽略,则可以使用Xcode 8.3中可用的新XCTAssertNoThrow函数:

  XCTAssertNoThrow(尝试x.doSomething()) 

有趣的是,在Xcode 8.3出现之前,我们有一个具有完全相同签名的自定义函数,除了删除自定义实现之外,我们无需更改任何代码。

另一种常见情况是,当返回的类型不是Equatable ,或者如果我们只想检查返回结果的某些属性。
即使类型是
当对象的描述很长时,相等的,相等的测试用例失败几乎是没有用的:很难确定哪个字段的值不正确:

在我们的项目中,我们有很多函数会产生不符合Equatable复杂类型。 一个常见的例子是数据模型对象-我们将它们公开为协议以隐藏内部实现,并且我们不希望它们是平等的。 每种模型类型都有一个带字典的引发初始化器。

在某个时候,我们意识到我们的单元测试看起来很可怕。 他们有重复的代码,没有意义的可选内容,并且当这些测试失败时,男孩们所有的东西都是红色的。 更糟糕的是,有很多复制粘贴。 代码如下:

  XCTAssertNoThrow(尝试BookObject(dictionary:sampleDictionary)) 
让书=尝试? BookObject(字典:sampleDictionary)
XCTAssertEqual(book?.name,“ ...”)
XCTAssertEqual(book?.description,“ ...”)
XCTAssertEqual(book?.rating,5)
...
//“更好的”版本:XCTAssertNoThrow(尝试BookObject(dictionary:sampleDictionary))
XCTAssertEqual(尝试BookObject(dictionary:sampleDictionary).name,“ ...”)
XCTAssertEqual(尝试BookObject(dictionary:sampleDictionary).description,“ ...”)
XCTAssertEqual(尝试BookObject(dictionary:sampleDictionary).rating,5)
...

结果证明很简单。 诀窍是提取XCTAssertNoThrow的第一个自动关闭产生的结果,然后对其执行附加验证关闭,但XCTAssertNoThrow是要有结果。

  public func XCTAssertNoThrow  (_ expression:@autoclosure()throws-> T,_ message:String =“”,file:StaticString = #file,line:UInt = #line,validatevalid:(T)-> Void ){func executeAndAssignResult (_ expression:@autoclosure()throws-> T,to:inout T?)重新抛出{ 
到=尝试expression()
}
var结果:T?
XCTAssertNoThrow(尝试executeAndAssignResult(表达式,至:&result),消息,文件:文件,行:行)
如果让r =结果{
validateResult(r)
}
}

现在,相同的测试看起来更加合理:可读性强,类型强并且生成可消化的消息。

引发特定错误

在某些情况下,我们想测试相反的情况-函数确实会引发错误。 测试模型反序列化通常需要此功能。
我们已经可以使用现有功能做到这一点
XCTAssertThrowsError ,尽管如果我们想检查是否抛出了某些特定错误,我们必须提供一个闭包来评估抛出的错误。

在查看我们通常在那里进行的检查时,我们注意到只有两种:将返回的错误与预期的错误进行比较,或者只是检查其类型。 因此,我们创建了两个便捷功能以将这些测试转换为单行代码:

 公共函数XCTAssertThrowsError  (_表达式:@autoclosure()抛出-> T,ExpectedError:E,_消息:字符串=“”,文件:StaticString = #file,行:UInt = #line ){XCTAssertThrowsError(尝试expression(),消息,文件:文件,行:行,{(错误)在 
XCTAssertNotNil(错误为?E,“ \(错误)不是\(E.self)”,文件:文件,行:行)
XCTAssertEqual(错误为E,期望错误,文件:文件,行:行)
})
}
公共函数XCTAssertThrowsError (_表达式:@autoclosure()抛出-> T,ExpectedErrorType:E.Type,_消息:String =“”,file:StaticString = #file,line:UInt = #line ){XCTAssertThrowsError(尝试expression(),消息,文件:文件,行:行,{(错误)在
XCTAssertNotNil(错误为?E,“ \(错误)不是\(E.self)”,文件:文件,行:行)
})
}

即使没有抛出…

投掷函数的功能还可以用于编写其他健壮的测试,这些测试还适用于其他情况,其中常规等式不适用。

考虑拥有一个不能等于的枚举-例如,如果其案例的关联值不是Equatable
而不是做
在测试用例中switch时,我们编写了纯的“ helper”函数,这些函数会引发有意义的错误。

一个常见的示例是结果枚举:

 列举结果{ 
成功案例([String:Any])
案例失败(错误)
}

如果我们正在测试直接返回Result的函数,则必须在错误的情况下switch返回的值并调用XCTFail 。 我们将为每个测试用例复制粘贴该开关,并且为新的枚举用例更新测试将是一场噩梦。

相反,我们可以创建一个辅助抛出函数来在一处处理枚举:

  XCTAssert(try result.assertIsSuccess(assertValue:{(value:[String:Any]])在 
XCTAssertEqual(value.count,10)
}))
XCTAssert(try result.assertIsFailure(assertError:{(value:Error)in
XCTAssertEquals(value,MyError.case)
}))
//标记:Helpersprivate扩展结果{
私人枚举错误:Swift.Error,CustomStringConvertible {
var讯息:字串
var说明:字符串{返回消息}
}
func assertIsSuccess (assertValue:(([[String:Any])throws-> Void)?= nil)throws-> Bool {
切换自我{
case .success(让值):
尝试使用assertValue?(value)
返回真
情况.failure(_):
引发错误(消息:“期望的。成功,得到了。\((自身)”))
}
}
func assertIsFailure (assertError:(((Error)throws-> Void)?= nil)throws-> Bool {
切换自我{
案例.success(_):
引发错误(消息:“预期的.failure,得到了。\((自身)”))
案例。失败(让错误):
尝试assertError?(值)
返回真
}
}
}

这种方法可用于各种情况,例如优雅地检查可选项(它们也是enums:troll :)。

关于创建自定义断言函数的说明

编写自定义测试功能时,要记住的事情很少。

  1. 最好添加linefile参数,并将它们一直传递到标准XCTAssert函数。 这样,测试用例失败将在调用您的自定义断言时报告,而不是在函数本身中报告。
  2. 最好添加message参数,以便调用方可以为测试提供上下文。 编写测试用例时也可以使用它们🙂
  3. XCTFail(message:)提供了一种使测试无条件失败的方法,这可能非常有用,例如,当测试不相等的枚举并陷入意外情况时。

在过去的一年中,XCTest框架已经变得非常强大,我们尝试尽可能多地重用现有功能,而不是复制其行为。

值得注意的是,为NSExceptions XCTest框架提供了更丰富的API,不幸的是,仅在Objective-C中可用:https://developer.apple.com/documentation/xctest/nsexception_assertions?language=objc

可以在以下位置找到所有断言功能的完整文档:https://developer.apple.com/documentation/xctest

最初的灵感来自于此:http://ericasadun.com/2017/05/27/tests-that-dont-crash/

Xcode 9的更新 :尚未添加任何新的断言API。 期待在XCTest的Swift版本中支持NSExceptions!

Xcode 10.2的更新 :尚未添加任何新的断言API。

我已经开始研究如何为开源实现做贡献,并在Swift论坛上创建了一个推销点。 您想帮我吗? 在此处或在Twitter上与我联系,让我们一起使XCTest更好!


谢谢阅读! 如果您喜欢这篇文章,请通过点击文章下方的“共享”按钮来分享 -更多的人将从中受益

您也可以在Twitter上关注我 ,我主要在其中撰写有关iOS开发的文章。

你想和我一起工作吗? Storytel正在招聘–在此申请🙌