MGParentSonProvider 0.1.0

MGParentSonProvider 0.1.0

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2017年8月
SwiftSwift 版本3.0
SPM支持 SPM

Undead1116 维护。



  • Harly

Nimble

使用 Nimble 表达 Swift 或 Objective-C 表达式的预期结果。受 Cedar 启发。

// Swift
expect(1 + 1).to(equal(2))
expect(1.2).to(beCloseTo(1.1, within: 0.1))
expect(3) > 2
expect("seahorse").to(contain("sea"))
expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi"))
expect(ocean.isClean).toEventually(beTruthy())

如何使用 Nimble

目录 DocToc 生成

一些背景:使用 XCTest 中的断言表达结果

Apple 的 Xcode 包含 XCTest 框架,它提供了断言宏来测试代码行为是否正常。例如,要断言 1 + 1 = 2,XCTest 会让您编写

// Swift

XCTAssertEqual(1 + 1, 2, "expected one plus one to equal two")

或者,在 Objective-C 中

// Objective-C

XCTAssertEqual(1 + 1, 2, @"expected one plus one to equal two");

XCTest 断言有几个缺点

  1. 宏不够。没有简单的方法可以断言一个字符串是否包含特定的子字符串,或者一个数字是否小于或等于另一个。
  2. 编写异步测试难度很大。 XCTest 强迫你编写大量的模板代码。

Nimble 解决了这些问题。

Nimble: 使用 expect(...).to 表达期望

Nimble 允许你使用一种自然、易于理解的语言来表达期望

// Swift

import Nimble

expect(seagull.squawk).to(equal("Squee!"))
// Objective-C

@import Nimble;

expect(seagull.squawk).to(equal(@"Squee!"));

expect 函数可以自动补全包含 file:line:,但这些参数是可选的。使用默认值,当期望未满足时,Xcode 会高亮正确的行。

要执行相反的期望——断言某个值不等于——可以使用 toNotnotTo

// Swift

import Nimble

expect(seagull.squawk).toNot(equal("Oh, hello there!"))
expect(seagull.squawk).notTo(equal("Oh, hello there!"))
// Objective-C

@import Nimble;

expect(seagull.squawk).toNot(equal(@"Oh, hello there!"));
expect(seagull.squawk).notTo(equal(@"Oh, hello there!"));

自定义失败消息

你希望在测试的失败消息中添加更多信息吗?使用可选参数 description 添加你自己的文本

// Swift

expect(1 + 1).to(equal(3))
// failed - expected to equal <3>, got <2>

expect(1 + 1).to(equal(3), description: "Make sure libKindergartenMath is loaded")
// failed - Make sure libKindergartenMath is loaded
// expected to equal <3>, got <2>

或者在 Objective-C 中使用 *WithDescription 版本

// Objective-C

@import Nimble;

expect(@(1+1)).to(equal(@3));
// failed - expected to equal <3.0000>, got <2.0000>

expect(@(1+1)).toWithDescription(equal(@3), @"Make sure libKindergartenMath is loaded");
// failed - Make sure libKindergartenMath is loaded
// expected to equal <3.0000>, got <2.0000>

类型安全

Nimble 确保你不会比较不匹配的两个类型

// Swift

// Does not compile:
expect(1 + 1).to(equal("Squee!"))

Nimble 使用泛型——仅限于 Swift——来确保类型正确。这意味着当在 Objective-C 中使用 Nimble 时,类型检查不可用。😭

运算符重载

厌倦了如此多的输入?在 Nimble 中,你可以使用重载的运算符,如 == 用于等价,或 > 用于比较

// Swift

// Passes if squawk does not equal "Hi!":
expect(seagull.squawk) != "Hi!"

// Passes if 10 is greater than 2:
expect(10) > 2

运算符重载仅限于 Swift,所以你无法在 Objective-C 中使用这种语法。💔

懒计算的值

expect 函数不会在给定时间时评估其给定的值,而是在需要匹配时才进行评估。所以 Nimble 可以测试表达式在被评估后是否抛出异常

// Swift

// Note: Swift currently doesn't have exceptions.
//       Only Objective-C code can raise exceptions
//       that Nimble will catch.
//       (see https://github.com/Quick/Nimble/issues/220#issuecomment-172667064)
let exception = NSException(
  name: NSInternalInconsistencyException,
  reason: "Not enough fish in the sea.",
  userInfo: ["something": "is fishy"])
expect { exception.raise() }.to(raiseException())

// Also, you can customize raiseException to be more specific
expect { exception.raise() }.to(raiseException(named: NSInternalInconsistencyException))
expect { exception.raise() }.to(raiseException(
    named: NSInternalInconsistencyException,
    reason: "Not enough fish in the sea"))
expect { exception.raise() }.to(raiseException(
    named: NSInternalInconsistencyException,
    reason: "Not enough fish in the sea",
    userInfo: ["something": "is fishy"]))

Objective-C 以同样的方式工作,但在对一个没有返回值的表达式做出期望时,你必须使用 expectAction

// Objective-C

NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException
                                                 reason:@"Not enough fish in the sea."
                                               userInfo:nil];
expectAction(^{ [exception raise]; }).to(raiseException());

// Use the property-block syntax to be more specific.
expectAction(^{ [exception raise]; }).to(raiseException().named(NSInternalInconsistencyException));
expectAction(^{ [exception raise]; }).to(raiseException().
    named(NSInternalInconsistencyException).
    reason("Not enough fish in the sea"));
expectAction(^{ [exception raise]; }).to(raiseException().
    named(NSInternalInconsistencyException).
    reason("Not enough fish in the sea").
    userInfo(@{@"something": @"is fishy"}));

// You can also pass a block for custom matching of the raised exception
expectAction(exception.raise()).to(raiseException().satisfyingBlock(^(NSException *exception) {
    expect(exception.name).to(beginWith(NSInternalInconsistencyException));
}));

C 语言原语

一些测试框架使测试原生 C 值变得困难。在 Nimble 中,这只是一个简单的事情

// Swift

let actual: CInt = 1
let expectedValue: CInt = 1
expect(actual).to(equal(expectedValue))

实际上,Nimble 使用类型推断,因此你可以写出上面的内容,而不需要显式指定两种类型

// Swift

expect(1 as CInt).to(equal(1))

在 Objective-C 中,Nimble 只支持 Objective-C 对象。要对原生 C 值做出期望,请将它们包裹在对象字面量中

expect(@(1 + 1)).to(equal(@2));

异步期望

在 Nimble 中,只需使用 toEventuallytoEventuallyNot,就可以很容易地在更新的异步值上做出期望。

// Swift 3.0 and later

DispatchQueue.main.async {
    ocean.add("dolphins")
    ocean.add("whales")
}
expect(ocean).toEventually(contain("dolphins", "whales"))
// Swift 2.3 and earlier

dispatch_async(dispatch_get_main_queue()) {
    ocean.add("dolphins")
    ocean.add("whales")
}
expect(ocean).toEventually(contain("dolphins", "whales"))
// Objective-C

dispatch_async(dispatch_get_main_queue(), ^{
    [ocean add:@"dolphins"];
    [ocean add:@"whales"];
});
expect(ocean).toEventually(contain(@"dolphins", @"whales"));

注意:toEventually 在主线程上触发轮询。阻塞主线程会导致 Nimble 停止运行循环。这可能导致在主线程上运行的任何不完整代码的测试污染。阻塞主线程可能是由于阻塞 IO、对 sleep() 的调用、死锁和同步 IPC。

在上面的例子中,ocean 持续被重新评估。如果它包含海豚和鲸鱼,则期望通过。即使 ocean 被连续重新评估整整一秒钟,它仍然不包含这些动物,期望也会失败。

有时值的更新需要超过一秒钟。在这些情况下,请使用 timeout 参数。

// Swift

// Waits three seconds for ocean to contain "starfish":
expect(ocean).toEventually(contain("starfish"), timeout: 3)

// Evaluate someValue every 0.2 seconds repeatedly until it equals 100, or fails if it timeouts after 5.5 seconds.
expect(someValue).toEventually(equal(100), timeout: 5.5, pollInterval: 0.2)
// Objective-C

// Waits three seconds for ocean to contain "starfish":
expect(ocean).withTimeout(3).toEventually(contain(@"starfish"));

你还可以通过使用 waitUntil 函数提供回调。

// Swift

waitUntil { done in
    // do some stuff that takes a while...
    NSThread.sleepForTimeInterval(0.5)
    done()
}
// Objective-C

waitUntil(^(void (^done)(void)){
    // do some stuff that takes a while...
    [NSThread sleepForTimeInterval:0.5];
    done();
});

waitUntil 还可以可选地接受超时参数

// Swift

waitUntil(timeout: 10) { done in
    // do some stuff that takes a while...
    NSThread.sleepForTimeInterval(1)
    done()
}
// Objective-C

waitUntilTimeout(10, ^(void (^done)(void)){
    // do some stuff that takes a while...
    [NSThread sleepForTimeInterval:1];
    done();
});

注意:waitUntil在主线程上触发超时代码。阻止主线程将导致Nimble停止运行循环以继续。这可能会造成主线程上未完成代码的测试污染。阻止主线程可能由阻塞I/O、调用sleep()、死锁和同步IPC引起。

在某些情况下(例如在较慢的机器上运行时),修改默认超时和轮询间隔值可能会有用。可以通过以下方式完成

// Swift

// Increase the global timeout to 5 seconds:
Nimble.AsyncDefaults.Timeout = 5

// Slow the polling interval to 0.1 seconds:
Nimble.AsyncDefaults.PollInterval = 0.1

Objective-C 支持

Nimble完全支持Objective-C。但是,在使用Objective-C中的Nimble时,有两个需要注意的事项

  1. 传递给expect函数的所有参数,以及像equal这样的匹配器函数,必须是Objective-C对象,或者可以转换为NSObject等价物。

    // Objective-C
    
    @import Nimble;
    
    expect(@(1 + 1)).to(equal(@2));
    expect(@"Hello world").to(contain(@"world"));
    
    // Boxed as NSNumber *
    expect(2).to(equal(2));
    expect(1.2).to(beLessThan(2.0));
    expect(true).to(beTruthy());
    
    // Boxed as NSString *
    expect("Hello world").to(equal("Hello world"));
    
    // Boxed as NSRange
    expect(NSMakeRange(1, 10)).to(equal(NSMakeRange(1, 10)));
  2. 要在一个不返回值的表达式上设置期望(例如-[NSException raise]),请使用expectAction而不是expect

    // Objective-C
    
    expectAction(^{ [exception raise]; }).to(raiseException());

以下类型目前被转换为NSObject类型

  • 数值类型转换为NSNumber *
  • NSRange转换为NSValue *
  • char *转换为NSString *

对于以下匹配器

  • equal
  • beGreaterThan
  • beGreaterThanOrEqual
  • beLessThan
  • beLessThanOrEqual
  • beCloseTo
  • beTrue
  • beFalse
  • beTruthy
  • beFalsy
  • haveCount

如果您想了解更多信息,请提交问题

禁用Objective-C简写语法

Nimble为使用expect函数表达期望提供了一种简写方式。要在Objective-C中禁用此简写,请在其代码中的某个位置定义NIMBLE_DISABLE_SHORT_SYNTAX宏,然后再导入Nimble。

#define NIMBLE_DISABLE_SHORT_SYNTAX 1

@import Nimble;

NMB_expect(^{ return seagull.squawk; }, __FILE__, __LINE__).to(NMB_equal(@"Squee!"));

如果您正在测试与Nimble函数(如expectequal)冲突的函数名称,则禁用简写是有用的。如果不是这种情况,禁用简写没有意义。

内置匹配器函数

Nimble包括大量匹配器函数。

类型检查

Nimble支持检查任何类型的对象是否属于某个类型,无论该对象是否遵循Objective-C。

// Swift

protocol SomeProtocol{}
class SomeClassConformingToProtocol: SomeProtocol{}
struct SomeStructConformingToProtocol: SomeProtocol{}

// The following tests pass
expect(1).to(beAKindOf(Int.self))
expect("turtle").to(beAKindOf(String.self))

let classObject = SomeClassConformingToProtocol()
expect(classObject).to(beAKindOf(SomeProtocol.self))
expect(classObject).to(beAKindOf(SomeClassConformingToProtocol.self))
expect(classObject).toNot(beAKindOf(SomeStructConformingToProtocol.self))

let structObject = SomeStructConformingToProtocol()
expect(structObject).to(beAKindOf(SomeProtocol.self))
expect(structObject).to(beAKindOf(SomeStructConformingToProtocol.self))
expect(structObject).toNot(beAKindOf(SomeClassConformingToProtocol.self))
// Objective-C

// The following tests pass
NSMutableArray *array = [NSMutableArray array];
expect(array).to(beAKindOf([NSArray class]));
expect(@1).toNot(beAKindOf([NSNull class]));

对象可以使用beAnInstanceOf匹配器来测试其确切的类型。

// Swift

protocol SomeProtocol{}
class SomeClassConformingToProtocol: SomeProtocol{}
struct SomeStructConformingToProtocol: SomeProtocol{}

// Unlike the 'beKindOf' matcher, the 'beAnInstanceOf' matcher only
// passes if the object is the EXACT type requested. The following
// tests pass -- note its behavior when working in an inheritance hierarchy.
expect(1).to(beAnInstanceOf(Int.self))
expect("turtle").to(beAnInstanceOf(String.self))

let classObject = SomeClassConformingToProtocol()
expect(classObject).toNot(beAnInstanceOf(SomeProtocol.self))
expect(classObject).to(beAnInstanceOf(SomeClassConformingToProtocol.self))
expect(classObject).toNot(beAnInstanceOf(SomeStructConformingToProtocol.self))

let structObject = SomeStructConformingToProtocol()
expect(structObject).toNot(beAnInstanceOf(SomeProtocol.self))
expect(structObject).to(beAnInstanceOf(SomeStructConformingToProtocol.self))
expect(structObject).toNot(beAnInstanceOf(SomeClassConformingToProtocol.self))

等价

// Swift

// Passes if 'actual' is equivalent to 'expected':
expect(actual).to(equal(expected))
expect(actual) == expected

// Passes if 'actual' is not equivalent to 'expected':
expect(actual).toNot(equal(expected))
expect(actual) != expected
// Objective-C

// Passes if 'actual' is equivalent to 'expected':
expect(actual).to(equal(expected))

// Passes if 'actual' is not equivalent to 'expected':
expect(actual).toNot(equal(expected))

值必须是EquatableComparableNSObject的子类。equal在用于比较一个或多个nil值时总是失败。

身份

// Swift

// Passes if 'actual' has the same pointer address as 'expected':
expect(actual).to(beIdenticalTo(expected))
expect(actual) === expected

// Passes if 'actual' does not have the same pointer address as 'expected':
expect(actual).toNot(beIdenticalTo(expected))
expect(actual) !== expected

重要的是要记住,只有当比较具有引用语义的类型时,beIdenticalTo才有意义,这些类型具有身份概念。在Swift中,这意味着定义为class的类型。

此匹配器在比较具有值语义的类型时不会工作,例如定义为structenum的类型。如果您需要比较两个值类型,请考虑您的类型实例相同的含义。这可能意味着比较单个属性,如果这样有意义,则可以将您的类型符合Equatable并使用Nimble的等价匹配器。

// Objective-C

// Passes if 'actual' has the same pointer address as 'expected':
expect(actual).to(beIdenticalTo(expected));

// Passes if 'actual' does not have the same pointer address as 'expected':
expect(actual).toNot(beIdenticalTo(expected));

比较

// Swift

expect(actual).to(beLessThan(expected))
expect(actual) < expected

expect(actual).to(beLessThanOrEqualTo(expected))
expect(actual) <= expected

expect(actual).to(beGreaterThan(expected))
expect(actual) > expected

expect(actual).to(beGreaterThanOrEqualTo(expected))
expect(actual) >= expected
// Objective-C

expect(actual).to(beLessThan(expected));
expect(actual).to(beLessThanOrEqualTo(expected));
expect(actual).to(beGreaterThan(expected));
expect(actual).to(beGreaterThanOrEqualTo(expected));

提供的比较匹配器的值必须实现Comparable

由于计算机如何表示浮点数,有时确保两个浮点数相等的主张会失败。要表达两个数应该在一定的误差范围内相互接近,请使用 beCloseTo

// Swift

expect(actual).to(beCloseTo(expected, within: delta))
// Objective-C

expect(actual).to(beCloseTo(expected).within(delta));

例如,要断言 10.01 接近于 10,可以编写

// Swift

expect(10.01).to(beCloseTo(10, within: 0.1))
// Objective-C

expect(@(10.01)).to(beCloseTo(@10).within(0.1));

Swift 还提供了一种快捷操作符

// Swift

expect(actual)  expected
expect(actual)  (expected, delta)

(在美式键盘上按 Option-x 可获得 ≈)

前一个版本使用默认的 delta 值为 0.0001。这里是另一种做法

// Swift

expect(actual)  expected ± delta
expect(actual) == expected ± delta

(在美式键盘上按 Option-Shift-= 可获得 ±)

如果比较的是浮点数的数组,下面的功能对你会很实用

// Swift

expect([0.0, 2.0])  [0.0001, 2.0001]
expect([0.0, 2.0]).to(beCloseTo([0.1, 2.1], within: 0.1))

传递给 beCloseTo 匹配器的值必须可以转换为 Double

类型/类

// Swift

// Passes if 'instance' is an instance of 'aClass':
expect(instance).to(beAnInstanceOf(aClass))

// Passes if 'instance' is an instance of 'aClass' or any of its subclasses:
expect(instance).to(beAKindOf(aClass))
// Objective-C

// Passes if 'instance' is an instance of 'aClass':
expect(instance).to(beAnInstanceOf(aClass));

// Passes if 'instance' is an instance of 'aClass' or any of its subclasses:
expect(instance).to(beAKindOf(aClass));

实例必须是 Objective-C 对象:是 NSObject 的子类,或者使用 @objc 前缀桥接到 Objective-C 的 Swift 对象。

例如,要断言 dolphin 是一种 Mammal

// Swift

expect(dolphin).to(beAKindOf(Mammal))
// Objective-C

expect(dolphin).to(beAKindOf([Mammal class]));

beAnInstanceOf 使用了 -[NSObject isMemberOfClass:] 方法来测试成员资格。beAKindOf 使用 -[NSObject isKindOfClass:]

真值性

// Passes if 'actual' is not nil, true, or an object with a boolean value of true:
expect(actual).to(beTruthy())

// Passes if 'actual' is only true (not nil or an object conforming to Boolean true):
expect(actual).to(beTrue())

// Passes if 'actual' is nil, false, or an object with a boolean value of false:
expect(actual).to(beFalsy())

// Passes if 'actual' is only false (not nil or an object conforming to Boolean false):
expect(actual).to(beFalse())

// Passes if 'actual' is nil:
expect(actual).to(beNil())
// Objective-C

// Passes if 'actual' is not nil, true, or an object with a boolean value of true:
expect(actual).to(beTruthy());

// Passes if 'actual' is only true (not nil or an object conforming to Boolean true):
expect(actual).to(beTrue());

// Passes if 'actual' is nil, false, or an object with a boolean value of false:
expect(actual).to(beFalsy());

// Passes if 'actual' is only false (not nil or an object conforming to Boolean false):
expect(actual).to(beFalse());

// Passes if 'actual' is nil:
expect(actual).to(beNil());

Swift 断言

如果你正在使用 Swift,可以使用 throwAssertion 匹配器来检查是否抛出了断言(例如 fatalError())。这是由 @mattgallagherCwlPreconditionTesting 库实现的。

// Swift

// Passes if 'somethingThatThrows()' throws an assertion, 
// such as by calling 'fatalError()' or if a precondition fails:
expect { try somethingThatThrows() }.to(throwAssertion())
expect { () -> Void in fatalError() }.to(throwAssertion())
expect { precondition(false) }.to(throwAssertion())

// Passes if throwing an NSError is not equal to throwing an assertion:
expect { throw NSError(domain: "test", code: 0, userInfo: nil) }.toNot(throwAssertion())

// Passes if the code after the precondition check is not run:
var reachedPoint1 = false
var reachedPoint2 = false
expect {
    reachedPoint1 = true
    precondition(false, "condition message")
    reachedPoint2 = true
}.to(throwAssertion())

expect(reachedPoint1) == true
expect(reachedPoint2) == false

注意

  • 此功能仅适用于 Swift。
  • 它仅适用于 x86_64 二进制文件,这意味着 您不能在 iOS 设备上运行此匹配器,只能运行模拟器
  • tvOS 模拟器受支持,但使用不同的机制,需要您取消 tvOS 方案的测试配置中的 Debug executable 方案设置。

Swift 错误处理

如果您正在使用 Swift 2.0 或更高版本,可以使用 throwError 匹配器来检查是否抛出了错误。

注意:以下代码示例引用了 Swift.Error 协议。在 Swift 3.0 之前的版本中,这是 Swift.ErrorProtocol

// Swift

// Passes if 'somethingThatThrows()' throws an 'Error':
expect { try somethingThatThrows() }.to(throwError())

// Passes if 'somethingThatThrows()' throws an error within a particular domain:
expect { try somethingThatThrows() }.to(throwError { (error: Error) in
    expect(error._domain).to(equal(NSCocoaErrorDomain))
})

// Passes if 'somethingThatThrows()' throws a particular error enum case:
expect { try somethingThatThrows() }.to(throwError(NSCocoaError.PropertyListReadCorruptError))

// Passes if 'somethingThatThrows()' throws an error of a particular type:
expect { try somethingThatThrows() }.to(throwError(errorType: NimbleError.self))

直接与 Error 值一起工作时,使用 matchError 匹配器允许您在不显式转换错误的情况下对错误本身执行某些检查。

matchError 匹配器允许您检查错误

  • 是否是您期望的错误类型。
  • 代表特定错误值,这是您期望的。

这在使用 ResultPromise 类型时很有用。

// Swift

let actual: Error = ...

// Passes if 'actual' represents any error value from the NimbleErrorEnum type:
expect(actual).to(matchError(NimbleErrorEnum.self))

// Passes if 'actual' represents the case 'timeout' from the NimbleErrorEnum type:
expect(actual).to(matchError(NimbleErrorEnum.timeout))

// Passes if 'actual' contains an NSError equal to the one provided:
expect(actual).to(matchError(NSError(domain: "err", code: 123, userInfo: nil)))

注意:此功能仅适用于 Swift。

异常

// Swift

// Passes if 'actual', when evaluated, raises an exception:
expect(actual).to(raiseException())

// Passes if 'actual' raises an exception with the given name:
expect(actual).to(raiseException(named: name))

// Passes if 'actual' raises an exception with the given name and reason:
expect(actual).to(raiseException(named: name, reason: reason))

// Passes if 'actual' raises an exception which passes expectations defined in the given closure:
// (in this case, if the exception's name begins with "a r")
expect { exception.raise() }.to(raiseException { (exception: NSException) in
    expect(exception.name).to(beginWith("a r"))
})
// Objective-C

// Passes if 'actual', when evaluated, raises an exception:
expect(actual).to(raiseException())

// Passes if 'actual' raises an exception with the given name
expect(actual).to(raiseException().named(name))

// Passes if 'actual' raises an exception with the given name and reason:
expect(actual).to(raiseException().named(name).reason(reason))

// Passes if 'actual' raises an exception and it passes expectations defined in the given block:
// (in this case, if name begins with "a r")
expect(actual).to(raiseException().satisfyingBlock(^(NSException *exception) {
    expect(exception.name).to(beginWith(@"a r"));
}));

注意:Swift 目前没有异常(请参阅 #220)。只有 Objective-C 代码可以引发 Nimble 捕获的异常。

集合成员资格

// Swift

// Passes if all of the expected values are members of 'actual':
expect(actual).to(contain(expected...))

// Passes if 'actual' is empty (i.e. it contains no elements):
expect(actual).to(beEmpty())
// Objective-C

// Passes if expected is a member of 'actual':
expect(actual).to(contain(expected));

// Passes if 'actual' is empty (i.e. it contains no elements):
expect(actual).to(beEmpty());

在 Swift 中 contain 可以接受任意数量的参数。如果所有这些参数都是集合的成员,则预期通过。在 Objective-C 中,目前 contain 只接受一个参数 (暂时如此)

例如,要断言海洋生物名称列表包含“dolphin”和“starfish”

// Swift

expect(["whale", "dolphin", "starfish"]).to(contain("dolphin", "starfish"))
// Objective-C

expect(@[@"whale", @"dolphin", @"starfish"]).to(contain(@"dolphin"));
expect(@[@"whale", @"dolphin", @"starfish"]).to(contain(@"starfish"));

containbeEmpty 期望集合是 NSArrayNSSet 或由 Equatable 元素组成的 Swift 集合的实例。

要测试一组元素是否在有序集合的开头或结尾,请使用 beginWithendWith

// Swift

// Passes if the elements in expected appear at the beginning of 'actual':
expect(actual).to(beginWith(expected...))

// Passes if the the elements in expected come at the end of 'actual':
expect(actual).to(endWith(expected...))
// Objective-C

// Passes if the elements in expected appear at the beginning of 'actual':
expect(actual).to(beginWith(expected));

// Passes if the the elements in expected come at the end of 'actual':
expect(actual).to(endWith(expected));

beginWithendWith 期望集合是 NSArray 或由 Equatable 元素组成的有序 Swift 集合的实例。

contain 类似,目前 Objective-C 中的 beginWithendWith 仅支持单个参数 (见此处)

对于返回没有严格排序的复杂对象集合的代码,可以使用 containElementSatisfying 匹配器

// Swift

struct Turtle {
    let color: String
}

let turtles: [Turtle] = functionThatReturnsSomeTurtlesInAnyOrder()

// This set of matchers passes regardless of whether the array is 
// [{color: "blue"}, {color: "green"}] or [{color: "green"}, {color: "blue"}]:

expect(turtles).to(containElementSatisfying({ turtle in
    return turtle.color == "green"
}))
expect(turtles).to(containElementSatisfying({ turtle in
    return turtle.color == "blue"
}, "that is a turtle with color 'blue'"))

// The second matcher will incorporate the provided string in the error message
// should it fail
// Objective-C

@interface Turtle : NSObject
@property (nonatomic, readonly, nonnull) NSString *color;
@end

@implementation Turtle 
@end

NSArray<Turtle *> * __nonnull turtles = functionThatReturnsSomeTurtlesInAnyOrder();

// This set of matchers passes regardless of whether the array is 
// [{color: "blue"}, {color: "green"}] or [{color: "green"}, {color: "blue"}]:

expect(turtles).to(containElementSatisfying(^BOOL(id __nonnull object) {
    return [[turtle color] isEqualToString:@"green"];
}));
expect(turtles).to(containElementSatisfying(^BOOL(id __nonnull object) {
    return [[turtle color] isEqualToString:@"blue"];
}));

字符串

// Swift

// Passes if 'actual' contains 'substring':
expect(actual).to(contain(substring))

// Passes if 'actual' begins with 'prefix':
expect(actual).to(beginWith(prefix))

// Passes if 'actual' ends with 'suffix':
expect(actual).to(endWith(suffix))

// Passes if 'actual' represents the empty string, "":
expect(actual).to(beEmpty())

// Passes if 'actual' matches the regular expression defined in 'expected':
expect(actual).to(match(expected))
// Objective-C

// Passes if 'actual' contains 'substring':
expect(actual).to(contain(expected));

// Passes if 'actual' begins with 'prefix':
expect(actual).to(beginWith(prefix));

// Passes if 'actual' ends with 'suffix':
expect(actual).to(endWith(suffix));

// Passes if 'actual' represents the empty string, "":
expect(actual).to(beEmpty());

// Passes if 'actual' matches the regular expression defined in 'expected':
expect(actual).to(match(expected))

集合元素

Nimble 提供了一种检查集合的每个元素是否满足某个预期的方法。

Swift

在 Swift 中,集合必须是符合 Sequence 类型约定的类型实例。

// Swift

// Providing a custom function:
expect([1, 2, 3, 4]).to(allPass { $0! < 5 })

// Composing the expectation with another matcher:
expect([1, 2, 3, 4]).to(allPass(beLessThan(5)))

Objective-C

在 Objective-C 中,集合必须是实现了 NSFastEnumeration 协议的类型实例,并且其元素是 NSObject 子类的实例。

此外,与 Swift 不同,没有重写以指定自定义匹配器函数。

// Objective-C

expect(@[@1, @2, @3, @4]).to(allPass(beLessThan(@5)));

集合计数

// Swift

// Passes if 'actual' contains the 'expected' number of elements:
expect(actual).to(haveCount(expected))

// Passes if 'actual' does _not_ contain the 'expected' number of elements:
expect(actual).notTo(haveCount(expected))
// Objective-C

// Passes if 'actual' contains the 'expected' number of elements:
expect(actual).to(haveCount(expected))

// Passes if 'actual' does _not_ contain the 'expected' number of elements:
expect(actual).notTo(haveCount(expected))

对于 Swift,实际值必须是符合 Collection 类型约定的类型实例。例如,ArrayDictionarySet 的实例。

对于 Objective-C,实际值必须是以下类之一,或它们的子类

  • NSArray,
  • NSDictionary,
  • NSSet,或者
  • NSHashTable.

通知

// Swift
let testNotification = Notification(name: "Foo", object: nil)

// passes if the closure in expect { ... } posts a notification to the default
// notification center.
expect {
    NotificationCenter.default.postNotification(testNotification)
}.to(postNotifications(equal([testNotification]))

// passes if the closure in expect { ... } posts a notification to a given
// notification center
let notificationCenter = NotificationCenter()
expect {
    notificationCenter.postNotification(testNotification)
}.to(postNotifications(equal([testNotification]), fromNotificationCenter: notificationCenter))

此匹配器仅可在 Swift 中使用。

将值与一组匹配器中的任何一个进行匹配

// Swift

// passes if actual is either less than 10 or greater than 20
expect(actual).to(satisfyAnyOf(beLessThan(10), beGreaterThan(20)))

// can include any number of matchers -- the following will pass
// **be careful** -- too many matchers can be the sign of an unfocused test
expect(6).to(satisfyAnyOf(equal(2), equal(3), equal(4), equal(5), equal(6), equal(7)))

// in Swift you also have the option to use the || operator to achieve a similar function
expect(82).to(beLessThan(50) || beGreaterThan(80))
// Objective-C

// passes if actual is either less than 10 or greater than 20
expect(actual).to(satisfyAnyOf(beLessThan(@10), beGreaterThan(@20)))

// can include any number of matchers -- the following will pass
// **be careful** -- too many matchers can be the sign of an unfocused test
expect(@6).to(satisfyAnyOf(equal(@2), equal(@3), equal(@4), equal(@5), equal(@6), equal(@7)))

注意:此匹配器允许您链式连接任意数量的匹配器。这提供了灵活性,但如果您在一个测试中链式连接了许多匹配器,那么考虑是否可以将该单个测试重构为多个更专注于特定内容的测试,以获得更好的覆盖率。

自定义验证

// Swift

// passes if .succeed is returned from the closure
expect({
    guard case .enumCaseWithAssociatedValueThatIDontCareAbout = actual else {
        return .failed("wrong enum case")
    }

    return .succeeded
}).to(succeed())

// passes if .failed is returned from the closure
expect({
    guard case .enumCaseWithAssociatedValueThatIDontCareAbout = actual else {
        return .failed("wrong enum case")
    }

    return .succeeded
}).notTo(succeed())

当测试失败时,使用 .failed() 提供的 String 将显示。

使用 toEventually() 时,请务必注意不要进行状态更改或运行资源密集型代码,因为此闭包将被多次执行。

编写您自己的匹配器

在 Nimble 中,匹配器是接受一个预期值并返回一个 Predicate 闭包的 Swift 函数。以 equal 为例

// Swift

public func equal<T: Equatable>(expectedValue: T?) -> Predicate<T> {
  // Can be shortened to:
  //   Predicate { actual in  ... }
  //
  // But shown with types here for clarity.
  return Predicate { (actual: Expression<T>) throws -> PredicateResult in
    let msg = ExpectationMessage.expectedActualValueTo("equal <\(expectedValue)>")
    if let actualValue = try actualExpression.evaluate() {
        return PredicateResult(
            bool: actualValue == expectedValue!,
            message: msg
        )
    } else {
        return PredicateResult(
            status: .fail,
            message: msg.appendedBeNilHint()
        )
    }
  }
}

Predicate 闭包的返回值是一个 PredicateResult,它指示实际值是否与预期匹配,以及失败时显示的错误信息。

实际的 equal 匹配器函数在 expected 为空时不会匹配;上面示例为了简洁而进行了编辑。

由于匹配器只是 Swift 函数,因此您可以在任何地方定义它们:在测试文件顶部、所有测试共享的文件中,或在您向其他人分发的 Xcode 项目中。

如果您编写的匹配器几乎人人可用,考虑将其添加到Nimble内置匹配器集合中,通过发送pull请求!或者通过GitHub自我分发。

要了解如何编写自己的匹配器,请查看Matchers目录,了解Nimble内置匹配器的实现方法。您还可以查看以下提示。

谓词结果

PredicateResult是谓词返回的结构体,用来指示成功和失败。一个PredicateResult由两个值组成:PredicateStatusExpectationMessage

与布尔值不同,PredicateStatus封装了三元值集合

// Swift

public enum PredicateStatus {
// The predicate "passes" with the given expression
// eg - expect(1).to(equal(1))
case matches

// The predicate "fails" with the given expression
// eg - expect(1).toNot(equal(1))
case doesNotMatch

// The predicate never "passes" with the given expression, even if negated
// eg - expect(nil as Int?).toNot(equal(1))
case fail

// ...
}

与此同时,ExpectationMessage提供了错误报告的消息语义。

// Swift

public indirect enum ExpectationMessage {
// Emits standard error message:
// eg - "expected to <string>, got <actual>"
case expectedActualValueTo(/* message: */ String)

// Allows any free-form message
// eg - "<string>"
case fail(/* message: */ String)

// ...
}

在报告错误时,谓词通常依赖于.expectedActualValueTo(..).fail(..)。其他枚举情况可以使用特殊案例。

最后,如果您的谓词利用了其他谓词,您可以使用.appended(details:).appended(message:)方法用更多详细信息注释现有错误。

一个常见的消息是关于失败的nil值。为此,可以使用.appendedBeNilHint()

惰性求值

actualExpression是围绕expect函数提供的值的懒memo化闭包。该表达式可以是闭包或直接传递给expect(...)的值。为了确定该值是否匹配,自定义匹配器应调用actualExpression.evaluate()

// Swift

public func beNil<T>() -> Predicate<T> {
    // Predicate.simpleNilable(..) automatically generates ExpectationMessage for
  // us based on the string we provide to it. Also, the 'Nilable' postfix indicates
  // that this Predicate supports matching against nil actualExpressions, instead of
  // always resulting in a PredicateStatus.fail result -- which is true for
  // Predicate.simple(..)
    return Predicate.simpleNilable("be nil") { actualExpression in
        let actualValue = try actualExpression.evaluate()
        return PredicateStatus(bool: actualValue == nil)
    }
}

在上面的示例中,actualExpression不是nil - 它是一个返回值的闭包。该值通过evaluate()方法访问,可能为nil。如果该值为nil,则beNil匹配器函数返回true,表示期望通过。

通过Swift泛型进行类型检查

使用Swift的泛型,匹配器可以通过修改返回类型来约束传递给expect函数的实际值的类型。

例如,以下匹配器haveDescription仅接受实现了Printable协议的实际值。它将其description与传递给匹配器函数的值进行比较,如果它们相同则通过。

// Swift

public func haveDescription(description: String) -> Predicate<Printable?> {
  return Predicate.simple("have description") { actual in
    return PredicateStatus(bool: actual.evaluate().description == description)
  }
}

自定义失败消息

当使用Predicate.simple(..)Predicate.simpleNilable(..)时,如果期望失败,Nimble将输出以下失败消息

// where `message` is the first string argument and
// `actual` is the actual value received in `expect(..)`
"expected to \(message), got <\(actual)>"

您可以通过修改创建Predicate的方式来自定义此消息。

基本自定义

对于稍复杂的错误消息,请使用Predicate.define(..)接收到创建的失败消息。

// Swift

public func equal<T: Equatable>(_ expectedValue: T?) -> Predicate<T> {
    return Predicate.define("equal <\(stringify(expectedValue))>") { actualExpression, msg in
        let actualValue = try actualExpression.evaluate()
        let matches = actualValue == expectedValue && expectedValue != nil
        if expectedValue == nil || actualValue == nil {
            if expectedValue == nil && actualValue != nil {
                return PredicateResult(
                    status: .fail,
                    message: msg.appendedBeNilHint()
                )
            }
            return PredicateResult(status: .fail, message: msg)
        }
        return PredicateResult(bool: matches, message: msg)
    }
}

在上面的示例中,msg是基于传递给Predicate.define的字符串定义的。代码看起来如下所示

// Swift

let msg = ExpectationMessage.expectedActualValueTo("equal <\(stringify(expectedValue))>")

完全自定义

要完全自定义谓词的行为,请使用期望返回一个PredicateResult的重载。

除了PredicateResult之外,还有其他可以使用ExpectationMessage枚举值

public indirect enum ExpectationMessage {
// Emits standard error message:
// eg - "expected to <message>, got <actual>"
case expectedActualValueTo(/* message: */ String)

// Allows any free-form message
// eg - "<message>"
case fail(/* message: */ String)

// Emits standard error message with a custom actual value instead of the default.
// eg - "expected to <message>, got <actual>"
case expectedCustomValueTo(/* message: */ String, /* actual: */ String)

// Emits standard error message without mentioning the actual value
// eg - "expected to <message>"
case expectedTo(/* message: */ String, /* actual: */ String)

// ...
}

对于由其他匹配器组成的匹配器,有一些辅助函数可以用于注释消息。

appended(message: String)用于添加到原始失败消息

// produces "expected to be true, got <actual> (use beFalse() for inverse)"
// appended message do show up inline in Xcode.
.expectedActualValueTo("be true").appended(message: " (use beFalse() for inverse)")

对于跨多行的更完整消息,请使用appended(details: String)代替

// produces "expected to be true, got <actual>\n\nuse beFalse() for inverse\nor use beNil()"
// details do not show inline in Xcode, but do show up in test logs.
.expectedActualValueTo("be true").appended(details: "use beFalse() for inverse\nor use beNil()")

支持 Objective-C

要从 Objective-C 使用 Swift 编写的自定义匹配器,您需要扩展 NMBObjCMatcher 类,为自定义匹配器添加一个新类的方。以下示例定义了类的方 +[NMBObjCMatcher beNilMatcher]

// Swift

extension NMBObjCMatcher {
  public class func beNilMatcher() -> NMBObjCMatcher {
    return NMBObjCMatcher { actualBlock, failureMessage, location in
      let block = ({ actualBlock() as NSObject? })
      let expr = Expression(expression: block, location: location)
      return beNil().matches(expr, failureMessage: failureMessage)
    }
  }
}

这允许您从 Objective-C 使用匹配器

// Objective-C

expect(actual).to([NMBObjCMatcher beNilMatcher]());

为了使语法更容易使用,定义一个调用类方法的 C 函数

// Objective-C

FOUNDATION_EXPORT id<NMBMatcher> beNil() {
  return [NMBObjCMatcher beNilMatcher];
}

在 Objective-C 匹配器中正确处理 nil

在支持 Objective-C 时,请确保适当地处理 nil。和 Cedar 一样,大多数匹配器并不与 nil 匹配。这是为了避免测试编写者在未期望的位置遇到 nil 值。

Nimble 为想要对 nil 对象做出期望的测试编写者提供了 beNil 匹配器功能

// Objective-C

expect(nil).to(equal(nil)); // fails
expect(nil).to(beNil());    // passes

如果您的匹配器不希望与 nil 匹配,您将使用 NonNilMatcherFuncNMBObjCMatcher 上的 canMatchNil 构造函数。同时使用这两种类型将在它们为 nil 时自动生成预期的值失败消息。

public func beginWith<S: Sequence, T: Equatable where S.Iterator.Element == T>(startingElement: T) -> NonNilMatcherFunc<S> {
    return NonNilMatcherFunc { actualExpression, failureMessage in
        failureMessage.postfixMessage = "begin with <\(startingElement)>"
        if let actualValue = actualExpression.evaluate() {
            var actualGenerator = actualValue.makeIterator()
            return actualGenerator.next() == startingElement
        }
        return false
    }
}

extension NMBObjCMatcher {
    public class func beginWithMatcher(expected: AnyObject) -> NMBObjCMatcher {
        return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage in
            let actual = actualExpression.evaluate()
            let expr = actualExpression.cast { $0 as? NMBOrderedCollection }
            return beginWith(expected).matches(expr, failureMessage: failureMessage)
        }
    }
}

从旧匹配器 API 迁移

之前(<7.0.0),Nimble 通过以下类型支持匹配器

  • 匹配器
  • 非 nil 匹配器函数
  • 匹配器函数

这些类型中的所有类型都已被 Predicate 替换。虽然迁移可能需要大量工作,但 Nimble 目前提供了几个步骤来帮助迁移您的自定义匹配器

最小步骤 - 使用 .predicate

Nimble 为旧类型提供了一个扩展,该扩展自动将这些类型转换为较新的 Predicate

// Swift
public func beginWith<S: Sequence, T: Equatable where S.Iterator.Element == T>(startingElement: T) -> Predicate<S> {
    return NonNilMatcherFunc { actualExpression, failureMessage in
        failureMessage.postfixMessage = "begin with <\(startingElement)>"
        if let actualValue = actualExpression.evaluate() {
            var actualGenerator = actualValue.makeIterator()
            return actualGenerator.next() == startingElement
        }
        return false
    }.predicate
}

这是支持 Predicate 的最简单方法,这允许更易于组合,而无需做出太多更改。

将转换为使用旧匹配器构造函数的 Predicate 类型

第二个最方便的步骤是利用 Predicate 支持的特殊构造函数,这些构造函数与旧 Nimble 匹配器类型的构造函数非常相似。

// Swift
public func beginWith<S: Sequence, T: Equatable where S.Iterator.Element == T>(startingElement: T) -> Predicate<S> {
    return Predicate.fromDeprecatedClosure { actualExpression, failureMessage in
        failureMessage.postfixMessage = "begin with <\(startingElement)>"
        if let actualValue = actualExpression.evaluate() {
            var actualGenerator = actualValue.makeIterator()
            return actualGenerator.next() == startingElement
        }
        return false
    }
}

这允许您从代码中完全删除旧类型,尽管预期的行为可能会略有改变。

使用首选构造函数将转换为 Predicate 类型

最后,您可以使用未用于帮助迁移的构造函数之一将转换为原生的 Predicate 格式。

弃用路线图

Nimble 7 引入了 Predicate,但仍将支持旧类型,并发出弃用警告。几个主要的 Nimble 发布版将保持与旧匹配器 API 的向后兼容性,尽管新功能可能不会回滚。

弃用计划是 3 个主要版本的删除。如下所示

  1. 引入新的 Predicate API,为旧匹配器 API 发出弃用警告。(Nimble v7.x.x
  2. 在迁移路径功能上引入警告(.predicate,与旧 API 类似的 Predicate-构造函数)。(Nimble v8.x.x
  3. 删除旧 API。(Nimble v9.x.x

安装 Nimble

Nimble可独立使用,也可与其姐妹项目Quick配合使用。要安装Quick和Nimble,请遵循Quick文档中的安装说明

Nimble目前可以通过两种方式之一安装:使用CocoaPods或使用git子模块。

将Nimble作为子模块安装

要将Nimble作为子模块用于测试您的macOS、iOS或tvOS应用程序,请按照以下4个简单的步骤进行操作:

  1. 克隆Nimble仓库
  2. 将Nimble.xcodeproj添加到您的项目Xcode工作区
  3. 将Nimble.framework链接到您的测试目标
  4. 开始编写期望值!

有关每个步骤的更详细说明,请阅读如何安装Quick。为了仅安装Nimble,请忽略涉及将Quick添加到您的项目的步骤。

在不使用XCTest的情况下使用Nimble

Nimble与XCTest集成,以便在使用Xcode测试包时能够良好工作,但它也可以在独立应用程序中使用。使用上述方法之一安装Nimble后,还需要两个额外的步骤才能使其工作。

  1. 创建一个自定义断言处理程序并将其实例分配给全局NimbleAssertionHandler变量。例如
class MyAssertionHandler : AssertionHandler {
    func assert(assertion: Bool, message: FailureMessage, location: SourceLocation) {
        if (!assertion) {
            print("Expectation failed: \(message.stringValue)")
        }
    }
}
// Somewhere before you use any assertions
NimbleAssertionHandler = MyAssertionHandler()
  1. 添加一个后构建操作以解决Swift XCTest支持库不需要地复制到您的应用程序的问题
  • 在Xcode中编辑您的配置方案,并转到构建 -> 后构建操作
  • 单击“+”图标并选择“新建运行脚本操作”
  • 打开“从以下位置提供构建设置”下拉菜单,并选择您的目标
  • 输入以下脚本内容
rm "${SWIFT_STDLIB_TOOL_DESTINATION_DIR}/libswiftXCTest.dylib"

现在您可以在代码中使用Nimble断言,并按您的喜好处理失败。