OCMockito 是 Mockito 的 Objective-C 实现,支持创建、验证和桩设模拟对象。
与其他模拟框架的主要区别
模拟对象总是“友好的”,会记录它们的调用,而不是抛出关于未指定调用的异常。 这使得测试更加健壮。
没有 expect-run-verify 模式,使测试更具可读性。 模拟对象记录它们的调用,然后您可以验证您想要的方法。
验证失败会报告为单元测试失败,标识特定行而不是抛出异常。 这使得识别失败更容易。
// mock creation
NSMutableArray *mockArray = mock([NSMutableArray class]);
// using mock object
[mockArray addObject:@"one"];
[mockArray removeAllObjects];
// verification
[verify(mockArray) addObject:@"one"];
[verify(mockArray) removeAllObjects];
创建后,模拟对象将记住所有交互。 然后您可以选择性地验证您感兴趣的任何交互。
(如果 Xcode 抱怨多个同名方法,请将 verify
转换为模拟的类。)
// mock creation
NSArray *mockArray = mock([NSArray class]);
// stubbing
[given([mockArray objectAtIndex:0]) willReturn:@"first"];
[given([mockArray objectAtIndex:1]) willThrow:[NSException exceptionWithName:@"name"
reason:@"reason"
userInfo:nil]];
// following prints "first"
NSLog(@"%@", [mockArray objectAtIndex:0]);
// follows throws exception
NSLog(@"%@", [mockArray objectAtIndex:1]);
// following prints "(null)" because objectAtIndex:999 was not stubbed
NSLog(@"%@", [mockArray objectAtIndex:999]);
__strong Class mockStringClass = mockClass([NSString class]);
(在 iOS 64 位运行时中,默认情况下 Class 对象不是强引用。将其显式设置为强引用,如上所示,或使用 id
代替。)
id <MyDelegate> delegate = mockProtocol(@protocol(MyDelegate));
或者,如果您不希望它包含任何可选方法
id <MyDelegate> delegate = mockProtocolWithoutOptionals(@protocol(MyDelegate));
UIViewController <CustomProtocol> *controller =
mockObjectAndProtocol([UIViewController class], @protocol(CustomProtocol));
要桩设返回基本标量的方法,请将标量装箱到 NSValue 中
[given([mockArray count]) willReturn:@3];
使用 willReturnStruct:objCType:
传递指向您的结构的指针及其来自 Objective-C @encode()
编译器指令的类型
SomeStruct aStruct = {...};
[given([mockObject methodReturningStruct]) willReturnStruct:&aStruct
objCType:@encode(SomeStruct)];
使用 stubProperty(mock, property, stubbedValue)
。 例如,假设您有一个名为 mockEmployee
的模拟对象。 它有一个属性 firstName
。 您希望将其桩设为返回值 "FIRST-NAME"
stubProperty(mockEmployee, firstName, @"FIRST-NAME");
这会桩设 firstName
属性、valueForKey:
和 valueForKeyPath:
。
OCMockito 通过测试相等性来验证参数值。 但是,当需要额外的灵活性时,您可以指定 OCHamcrest 匹配器。
// mock creation
NSMutableArray *mockArray = mock([NSMutableArray class]);
// using mock object
[mockArray removeObject:@"This is a test"];
// verification
[verify(mockArray) removeObject:startsWith(@"This is")];
OCHamcrest 匹配器可以指定为验证和桩设的参数。
类型化参数将发出警告,提示匹配器类型错误。 只需将匹配器强制转换为 id
。
要桩设一个接受非对象参数但指定匹配器的方法,请使用虚拟参数调用该方法,然后调用 -withMatcher:forArgument:
[[given([mockArray objectAtIndex:0]) withMatcher:anything() forArgument:0]
willReturn:@"foo"];
这对于忽略 NSError **
参数特别有用:传入 NULL
,但使用 anything()
匹配器覆盖它。
使用快捷方式 -withMatcher:
为单个参数指定匹配器
[[given([mockArray objectAtIndex:0]) withMatcher:anything()]
willReturn:@"foo"];
这些方法也可用于为验证指定匹配器。 只需在 verify(…)
之后但在您要验证的调用之前调用它们
[[verify(mockArray) withMatcher:greaterThan(@5])] removeObjectAtIndex:0];
// using mock
[mockArray addObject:@"once"];
[mockArray addObject:@"twice"];
[mockArray addObject:@"twice"];
// the following two verifications work exactly the same
[verify(mockArray) addObject:@"once"];
[verifyCount(mockArray, times(1)) addObject:@"once"];
// verify exact number of invocations
[verifyCount(mockArray, times(2)) addObject:@"twice"];
[verifyCount(mockArray, times(3)) addObject:@"three times"];
// verify using never(), which is an alias for times(0)
[verifyCount(mockArray, never()) addObject:@"never happened"];
// verify using atLeast()/atMost()
[verifyCount(mockArray, atLeastOnce()) addObject:@"at least once"];
[verifyCount(mockArray, atLeast(2)) addObject:@"at least twice"];
[verifyCount(mockArray, atMost(5)) addObject:@"at most five times"];
OCMockito 使用 OCHamcrest 匹配器验证参数值; 非匹配器参数隐式包装在 equalTo
匹配器中以测试相等性。 但是在某些情况下,捕获一个参数以便您可以向它发送另一个消息很有帮助。
OCHamcrest 为此目的提供了一个特殊的匹配器:HCArgumentCaptor。 将其指定为参数,然后使用 value
或 allValues
属性查询它。
例如,您可能想要向捕获的参数发送一条消息以查询其状态
HCArgumentCaptor *argument = [[HCArgumentCaptor alloc] init];
[verify(mockObject) doSomething:(id)argument];
assertThat([argument.value nameAtIndex:0], is(@"Jon"));
捕获参数对于 block 参数尤其方便。 捕获参数,将其强制转换为 block 类型,然后直接调用该 block 以模拟生产代码将调用它的方式
HCArgumentCaptor *argument = [[HCArgumentCaptor alloc] init];
[verify(mockArray) sortUsingComparator:(id)argument];
NSComparator block = argument.value;
assertThat(@(block(@"a", @"z")), is(@(NSOrderedAscending)));
[[given([mockObject someMethod:@"some arg"])
willThrow:[NSException exceptionWithName:@"name" reason:@"reason" userInfo:nil]]
willReturn:@"foo"];
// First call: throws exception
[mockObject someMethod:@"some arg"];
// Second call: prints "foo"
NSLog(@"%@", [mockObject someMethod:@"some arg"]);
// Any consecutive call: prints "foo" as well. (Last stubbing wins.)
NSLog(@"%@", [mockObject someMethod:@"some arg"]);
我们建议仅使用带有 willReturn:
或 willThrow:
的简单桩设。 但是使用 block 的 willDo:
有时可能会有所帮助。 该 block 可以通过从 NSInvocation+OCMockito.h 调用 mkt_arguments
轻松访问调用参数。 block 返回的任何内容都将用作桩设的返回值。
[given([mockObject someMethod:anything()]) willDo:^id (NSInvocation *invocation){
NSArray *args = [invocation mkt_arguments];
return @([args[0] intValue] * 2);
}];
// Following prints 4
NSLog(@"%@", [mockObject someMethod:@2]);
您可以使用 givenVoid
而不是 given
,使用 block 来桩设一个 void 方法。
如果您的 System Under Test 的 -dealloc
试图向一个被模拟的对象发送消息,请使用 stopMocking(…)
。 它会禁用模拟对象上的消息处理并释放其保留的参数。 这可以防止保留周期和测试清理期间的崩溃。 有关示例,请参见 StopMockingTests.m。
简短的答案是:不要这样做。 不要让您的类决定与谁对话,而是注入这些依赖项。
更长的答案是:好吧,遗留代码。 在模拟类对象上调用 stubSingleton
,指定工厂方法的名称。
__strong Class mockUserDefaultsClass = mockClass([NSUserDefaults class]);
NSUserDefaults* mockDefaults = mock([NSUserDefaults class]);
stubSingleton(mockUserDefaultsClass, standardUserDefaults);
[given([NSUserDefaults standardUserDefaults]) willReturn:mockDefaults];
小心! 这使用了方法调剂 (swizzling)。 您需要确保模拟类对象被释放,以便撤消方法调剂。
在上面的示例中,mockUserDefaultsClass
将超出范围并被销毁。 但是,如果您将其保留在测试装置中,作为 ivar 或属性呢? 根据 XCTest 的设计,它不会被隐式销毁。 您需要在 -tearDown
中显式将其设置为 nil,否则方法调剂将蔓延到您的其他测试中,从而损害它们的完整性。
如果您需要更多地控制何时撤消方法调剂,请在模拟类上调用 stopMocking(…)
。
Examples 文件夹显示了可以通过 Swift Package Manager、CocoaPods 或预构建框架使用 OCMockito 的项目。
在您的 Package.swift 清单的 dependencies
数组中包含一个 OCMockito 包
dependencies: [
.package(
url: "https://github.com/jonreid/OCMockito",
.upToNextMajor(from: "7.0.0")
),
],
然后将 OCMockito 添加到 .testTarget
的依赖项中
.testTarget(
name: "ExampleTests",
dependencies: [
"Example",
"OCMockito",
]
),
如果您想使用 Cocoapods 添加 OCMockito,请将以下依赖项添加到您的 Podfile。 大多数人希望在他们的测试目标中使用 OCMockito,而不是包含来自他们主要目标的任何 pods
target 'MyTests' do
inherit! :search_paths
use_frameworks!
pod 'OCMockito', '~> 7.0'
end
将以下内容添加到您的 Cartfile
github "jonreid/OCMockito" ~> 7.0
然后将构建的框架(OCHamcrest 和 OCMockito)从相应的 Carthage/Build 目录拖到您的项目中,但禁用“将项目复制到目标组的文件夹中”。
GitHub 上提供了 OCMockito 的预构建二进制文件。 您还需要 OCHamcrest。 该二进制文件打包为 OCMockito.xcframework,包含以下架构
将 XCFramework 拖到您的项目中。
如果您想自己构建 OCMockito,请克隆 repo,然后
$ cd Source
$ ./MakeDistribution.sh
Jon Reid 是 iOS Unit Testing by Example 的作者。 他的网站是 Quality Coding。