待办:构建示例项目。
Spry
Spry是一个框架,允许在Apple的Swift语言中进行窥探和存根。还包括一个Nimble匹配器,用于存根的对象。
目录
动机
在为类编写测试时,建议只测试该类的行为,而不是它所使用的其他对象。在Swift中,这可能很困难。
您如何检查您是否在正确的时间和位置调用正确的方法,并且传递了适当的参数?Spry允许您轻松创建一个记录每个调用函数及其参数的间谍对象。
您如何确保注入的对象将为给定测试返回必要的值?Spry允许您轻松创建一个可以返回特定值的存根对象。
这样,您可以从前端测试您正在测试的类(测试对象)的角度编写测试,并且不再需要其他任何东西。
Spryable
同时遵循 Stubbable 和 Spyable!关于 Stubbable 和 Spyable 的信息,请参阅以下各自的章节。
功能
- 同时遵循
Spyable
和Stubbable
。 - 使用
resetCallsAndStubs()
同时重置调用和存根。 - 实现简单
- 创建一个符合
Spryable
的对象 - 在每个函数(应该是存根和监视的函数)中,返回将所有参数(如果有的话)传递给
spryify()
的结果- 也适用于特殊函数,如
subscript
- 也适用于特殊函数,如
- 在每个属性(应该是存根和监视的属性)中,在
get {}
中返回stubbedValue()
的结果,并在set {}
中使用recordCall()
- 创建一个符合
Spryable 示例
// The Real Thing can be a protocol
protocol StringService: class {
var readonlyProperty: String { get }
var readwriteProperty: String { set get }
func giveMeAString(arg1: Bool, arg2: String) -> String
static func giveMeAString(arg1: Bool, arg2: String) -> String
}
// The Real Thing can be a class
class StringService {
var readonlyProperty: String {
return ""
}
var readwriteProperty: String = ""
func giveMeAString(arg1: Bool, arg2: String) -> String {
// do real things
return ""
}
static func giveMeAString(arg1: Bool, arg2: String) -> String {
// do real things
return ""
}
}
// The Fake Class (If the fake is from a class then `override` will be required for each function and property)
class FakeStringService: StringService, Spryable {
enum ClassFunction: String, StringRepresentable { // <-- **REQUIRED**
case giveMeAString = "giveMeAString(arg1:arg2:)"
}
enum Function: String, StringRepresentable { // <-- **REQUIRED**
case readonlyProperty = "readonlyProperty"
case readwriteProperty = "readwriteProperty"
case giveMeAString = "giveMeAString(arg1:arg2:)"
}
var readonlyProperty: String {
return stubbedValue()
}
var readwriteProperty: String {
set {
recordCall(arguments: newValue)
}
get {
return stubbedValue()
}
}
func giveMeAString(arg1: Bool, arg2: String) -> String {
return spryify(arguments: arg1, arg2) // <-- **REQUIRED**
}
static func giveMeAString(arg1: Bool, arg2: String) -> String {
return spryify(arguments: arg1, arg2) // <-- **REQUIRED**
}
}
Stubbable
遵循 Spryable 将同时遵循 Stubbable 和 Spyable。
功能
- 使用
.andReturn()
在类的实例或类本身上存根函数的返回值 - 使用
.andDo()
在类的实例或类本身上存根函数的实现 - 使用
.with()
指定只有传递了正确的参数才使用的存根(关于其他指定方式,请参阅 参数枚举) - 添加详细的存根函数列表,如果未找到存根或收到的参数未通过验证,则包含丰富的
fatalityError()
信息 - 使用
resetStubs()
重置存根 - 实现简单
- 创建一个符合
Stubbable
的对象 - 在每个函数(应该是存根的函数)中,返回将所有参数(如果有的话)传递给
stubbedValue()
的结果
- 创建一个符合
存根没有返回值的函数时,使用
Void()
作为存根返回值
Stubbable 示例
// The Real Thing can be a protocol
protocol StringService: class {
var readonlyProperty: String { get }
var readwriteProperty: String { set get }
func giveMeAString() -> String
func hereAreTwoStrings(string1: String, string2: String) -> Bool
func iHaveACompletionClosure(string: String, completion: () -> Void)
static func imAClassFunction()
}
// The Real Thing can be a class
class StringService {
var readonlyProperty: String {
return ""
}
var readwriteProperty: String = ""
func giveMeAString() -> String {
// do real things
return "string"
}
func hereAreTwoStrings(string1: String, string2: String) -> Bool {
// do real things
return true
}
func iHaveACompletionClosure(string: String, completion: () -> Void) {
// do real things
}
static func imAClassFunction() {
// do real things
}
}
// The Stub Class (If the fake is from a class then `override` will be required for each function and property)
class FakeStringService: StringService, Stubbable {
enum ClassFunction: String, StringRepresentable { // <-- **REQUIRED**
case imAClassFunction = "imAClassFunction()"
}
enum Function: String, StringRepresentable { // <-- **REQUIRED**
case readonlyProperty = "readonlyProperty"
case readwriteProperty = "readwriteProperty"
case giveMeAString = "giveMeAString()"
case hereAreTwoStrings = "hereAreTwoStrings(string1:string2:)"
}
var readonlyProperty: String {
return stubbedValue()
}
var readwriteProperty: String {
get {
return stubbedValue()
}
}
func giveMeAString() -> String {
return stubbedValue() // <-- **REQUIRED**
}
func hereAreTwoStrings(string1: String, string2: String) -> Bool {
return stubbedValue(arguments: string1, string2) // <-- **REQUIRED**
}
func iHaveACompletionClosure(string: String, completion: () -> Void) {
return stubbedValue(arguments: string, completion) // <-- **REQUIRED**
}
static func imAClassFunction() {
return stubbedValue() // <-- **REQUIRED**
}
}
模拟示例
// will always return `"stubbed value"`
fakeStringService.stub(.hereAreTwoStrings).andReturn("stubbed value")
// specifying all arguments (will only return `true` if the arguments passed in match "first string" and "second string")
fakeStringService.stub(.hereAreTwoStrings).with("first string", "second string").andReturn(true)
// using the Arguement enum (will only return `true` if the second argument is "only this string matters")
fakeStringService.stub(.hereAreTwoStrings).with(Argument.anything, "only this string matters").andReturn(true)
// using custom validation
let customArgumentValidation = Argument.pass({ actualArgument -> Bool in
let passesCustomValidation = // ...
return passesCustomValidation
})
fakeStringService.stub(.hereAreTwoStrings).with(Argument.anything, customArgumentValidation).andReturn("stubbed value")
// using argument captor
let captor = Argument.captor()
fakeStringService.stub(.hereAreTwoStrings).with(Argument.nonNil, captor).andReturn("stubbed value")
captor.getValue(as: String.self) // gets the second argument the first time this function was called where the first argument was also non-nil.
captor.getValue(at: 1, as: String.self) // // gets the second argument the second time this function was called where the first argument was also non-nil.
// using `andDo()` - Also has the ability to specify the arguments!
fakeStringService.stub(.iHaveACompletionClosure).with("correct string", Argument.anything).andDo({ arguments in
// get the passed in argument
let completionClosure = arguments[0] as! () -> Void
// use the argument
completionClosure()
// return an appropriate value
return Void() // <-- will be returned by the stub
})
// can stub static functions as well
FakeStringService.stub(.imAClassFunction).andReturn(Void())
可监视
遵循 Spryable 将同时遵循 Stubbable 和 Spyable。
功能
- 测试一个函数是否被调用或实例或类的属性是否被设置
- 指定调用时应接收的参数(有关备用规范,请参阅参数枚举)
- 包含详细的函数和参数列表的丰富故障消息
- 使用
resetCalls()
重置调用 - 实现简单
- 创建一个符合
Spyable
的对象 - 在每一个函数(应监视的函数)调用
recordCall()
,传递所有参数(如果有)
- 创建一个符合
当使用协议声明对象的接口时,编译器会告诉您“假”何时不符合规范。
可监视示例
// The Real Thing can be a protocol
protocol StringService: class {
var readonlyProperty: String { get }
var readwriteProperty: String { set get }
func giveMeAString() -> String
func hereAreTwoStrings(string1: String, string2: String) -> Bool
static func imAClassFunction()
}
// The Real Thing can be a class
class RealStringService: StringService {
var readonlyProperty: String {
return ""
}
var readwriteProperty: String = ""
func giveMeAString() -> String {
// do real things
return "string"
}
func hereAreTwoStrings(string1: String, string2: String) -> Bool {
// do real things
return true
}
static func imAClassFunction() {
// do real things
}
}
// The Spy Class (If the fake is from a class then `override` will be required for each function and property)
class FakeStringService: StringService, Spyable {
enum ClassFunction: String, StringRepresentable { // <-- **REQUIRED**
case imAClassFunction = "imAClassFunction()"
}
enum Function: String, StringRepresentable { // <-- **REQUIRED**
case readwriteProperty = "readwriteProperty"
case giveMeAString = "giveMeAString()"
case hereAreTwoStrings = "hereAreTwoStrings(string1:string2:)"
}
var readonlyProperty: String {
return ""
}
var readwriteProperty: String {
set {
recordCall(arguments: newValue)
}
get {
return ""
}
}
func giveMeAString() -> String {
recordCall() // <-- **REQUIRED**
return ""
}
func hereAreTwoStrings(string1: String, string2: String) -> Bool {
recordCall(arguments: string1, string2) // <-- **REQUIRED**
return false
}
static func imAClassFunction() {
recordCall() // <-- **REQUIRED**
}
}
被调用示例
结果
// the result
let result = spyable.didCall(.functionName)
// was the function called on the fake?
result.success
// what was called on the fake?
result.recordedCallsDescription
如何使用
// passes if the function was called
fake.didCall(.functionName).success
// passes if the function was called a number of times
fake.didCall(.functionName, countSpecifier: .exactly(1)).success
// passes if the function was called at least a number of times
fake.didCall(.functionName, countSpecifier: .atLeast(1)).success
// passes if the function was called at most a number of times
fake.didCall(.functionName, countSpecifier: .atMost(1)).success
// passes if the function was called with equivalent arguments
fake.didCall(.functionName, withArguments: ["firstArg", "secondArg"]).success
// passes if the function was called with arguments that pass the specified options
fake.didCall(.functionName, withArguments: [Argument.nonNil, Argument.anything, "thirdArg"]).success
// passes if the function was called with an argument that passes the custom validation
let customArgumentValidation = Argument.pass({ argument -> Bool in
let passesCustomValidation = // ...
return passesCustomValidation
})
fake.didCall(.functionName, withArguments: [customArgumentValidation]).success
// passes if the function was called with equivalent arguments a number of times
fake.didCall(.functionName, withArguments: ["firstArg", "secondArg"], countSpecifier: .exactly(1)).success
// passes if the property was set to the right value
fake.didCall(.propertyName, with: "value").success
// passes if the static function was called
Fake.didCall(.functionName).success
已接收匹配器
“已接收匹配器”与Nimble一起使用。该匹配器是名为“Spry+Nimble”的独立cocoapod的一部分。以下为安装说明。
所有调用匹配器都可以与to()
和toNot()
一起使用
已接收示例
// passes if the function was called
expect(fake).to(haveReceived(.functionName)
// passes if the function was called a number of times
expect(fake).to(haveReceived(.functionName, countSpecifier: .exactly(1)))
// passes if the function was called at least a number of times
expect(fake).to(haveReceived(.functionName, countSpecifier: .atLeast(2)))
// passes if the function was called at most a number of times
expect(fake).to(haveReceived(.functionName, countSpecifier: .atMost(1)))
// passes if the function was called with equivalent arguments
expect(fake).to(haveReceived(.functionName, with: "firstArg", "secondArg"))
// passes if the function was called with arguments that pass the specified options
expect(fake).to(haveReceived(.functionName, with: Argument.nonNil, Argument.anything, "thirdArg"))
// passes if the function was called with an argument that passes the custom validation
let customArgumentValidation = Argument.pass({ argument -> Bool in
let passesCustomValidation = // ...
return passesCustomValidation
})
expect(fake).to(haveReceived(.functionName, with: customArgumentValidation))
// passes if the function was called with equivalent arguments a number of times
expect(fake).to(haveReceived(.functionName, with: "firstArg", "secondArg", countSpecifier: .exactly(1)))
// passes if the property was set to the specified value
expect(fake).to(haveReceived(.propertyName, with "value"))
// passes if the static function was called
expect(Fake).to(haveReceived(.functionName))
// passes if the static property was set
expect(Fake).to(haveReceived(.propertyName))
SpryEquatable
Spry使用SpryEquatable
协议来比较参数
- 使用单独一行声明协议遵从性以及以下之一来使类型遵从
SpryEquatable
- 任意物体
- 所有
class
都是AnyObject
enum
和struct
不是AnyObject
- 所有
- 遵从Swift的
Equatable
协议- 要使自定义类型遵从
Equatable
,请参阅Apple的文档: Equatable - 注意:如果忘记遵从
Equatable
,编译器只会告诉你没有遵从SpryEquatable
(你不应该实现SpryEquatable
中声明的任何方法)
- 要使自定义类型遵从
- 任意物体
- 注意:既是
AnyObject
又遵从Equatable
的对象,将使用指针比较,而不是Equatable
的==(lhs:right:)
函数。
默认遵从列表
- 可选的(如果包装的类型不遵从SpryEquatable,则在运行时将引发
fatalError()
) - 字符串
- 整型
- 双精度浮点数
- 布尔型
- 数组
- 字典
NSObject
SpryEquatable遵从示例
// custom type
extension Person: Equatable, SpryEquatable {
public state func == (lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name
&& lhs.age == rhs.age
}
}
// existing type that is already Equatable
extension String: SpryEquatable {}
ArgumentEnum
当使用Equatable
协议精确比较参数不是期望、需要或可行时使用。
case anything
- 用于表示传入的绝对任何内容都足够。
case nonNil
- 用于表示传入的任何非空内容都足够。
case nil
- 用于表示只有传入nil才足够。
case pass((Any?) -> Bool)
- 用于为特定参数提供自定义验证。
func captor() -> ArgumentCaptor
- 用于创建一个新的ArgumentCaptor
ArgumentCaptor
当模拟的函数被调用时,ArgumentCaptor用于捕获特定的参数。之后,捕获器可以为自定义参数检查提供被捕获的参数。ArgumentCaptor会在模拟的函数被调用时捕获指定的参数。
捕获的参数以每个函数调用的顺序存储。当获取参数时,您可以指定获取哪个参数(默认为第一次调用函数时的时间)
获取捕获的参数时必须指定类型。如果无法将参数转换为指定的类型,则将发生 fatalError()
。
ArgumentCaptor 示例
let captor = Argument.captor()
fakeStringService.stub(.hereAreTwoStrings).with(Argument.anything, captor).andReturn("stubbed value")
_ = fakeStringService.hereAreTwoStrings(string1: "first arg first call", string2: "second arg first call")
_ = fakeStringService.hereAreTwoStrings(string1: "first arg second call", string2: "second arg second call")
let secondArgFromFirstCall = captor.getValue(as: String.self) // `at:` defaults to `0` or first call
let secondArgFromSecondCall = captor.getValue(at: 1, as: String.self)
Xcode 模板
创建虚假对象的模板可在此存储库的 "Templates" 文件夹中找到。
用法
当您在 Xcode 中创建新文件时,您会注意到一个新的部分称为 "Spry",包含一个名为 "Spry Fake" 的模板。选择模板,点击 "Next",输入您想要模拟的类型或协议的名称("Fake" 将自动添加),选择虚假的目标和文件夹位置,然后开始测试!
模板安装
在终端运行以下命令
svn export https://github.com/Rivukis/Spry/trunk/Templates/Spry ~/Library/Developer/Xcode/Templates/File\ Templates/Spry
安装
Spry 和 Spry+Nimble 可通过 CocoaPods 获得使用。要安装,只需将它们添加到您的 Podfile。
platform :ios, '9.0'
source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!
target "<YOUR_TARGET>" do
target '<YOUR_TARGET>Tests' do
inherit! :search_paths
pod 'Spry'
#Uncomment the following lines to import Quick/Nimble as well as a Nimble Matcher used to test if a 'fake' has received function calls.
#pod 'Quick'
#pod 'Nimble'
#pod 'Spry+Nimble'
end
end
贡献
如果您有使 Spry 更好的想法,请及时提交拉取请求!
许可证
MIT 许可证
版权所有 (c) [2017] [Brian Radebaugh]
本澄清函特此授予任何获取本软件及其相关文档文件的副本(以下简称“软件”)的人,在不受限制的条件下处理软件的权利,包括但不限于使用、复制、修改、合并、发布、分发、许可和/或销售软件副本的权利,并允许向软件提供方提供软件的人进行此类操作,但须遵守以下条件:
上述版权通知和本许可通知应包含在软件的任何副本或实质性部分中。
软件按“原样”提供,不提供任何形式的保证,无论是明示的还是暗示的,包括但不限于适销性、特定用途适用性和非侵权性。在任何情况下,作者或版权持有人对因软件本身、使用软件或与软件相关的任何操作而产生的任何索赔、损害或其他责任,均不负任何责任,无论是基于合同、侵权或其他原因。