mockito

OCMockito

Build Status Coverage Status Swift Package Index Platform Compatibility Carthage compatible CocoaPods Version Mastodon Follow

OCMockito 是 Mockito 的 Objective-C 实现,支持创建、验证和桩设模拟对象。

与其他模拟框架的主要区别

目录

让我们验证一些行为!

// 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)];

如何桩设一个属性,以便 KVO 正常工作?

使用 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];

验证确切的调用次数 / 至少 x 次 / 从不

// 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。 将其指定为参数,然后使用 valueallValues 属性查询它。

例如,您可能想要向捕获的参数发送一条消息以查询其状态

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"]);

使用 block 进行桩设

我们建议仅使用带有 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 方法。

dealloc 的问题

如果您的 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(…)

如何将 OCMockito 添加到我的项目中?

Examples 文件夹显示了可以通过 Swift Package Manager、CocoaPods 或预构建框架使用 OCMockito 的项目。

Swift Package Manager

在您的 Package.swift 清单的 dependencies 数组中包含一个 OCMockito 包

dependencies: [
    .package(
        url: "https://github.com/jonreid/OCMockito",
        .upToNextMajor(from: "7.0.0")
    ),
],

snippet source | anchor

然后将 OCMockito 添加到 .testTarget 的依赖项中

.testTarget(
    name: "ExampleTests",
    dependencies: [
        "Example",
        "OCMockito",
    ]
),

snippet source | anchor

CocoaPods

如果您想使用 Cocoapods 添加 OCMockito,请将以下依赖项添加到您的 Podfile。 大多数人希望在他们的测试目标中使用 OCMockito,而不是包含来自他们主要目标的任何 pods

target 'MyTests' do
  inherit! :search_paths
  use_frameworks!
  pod 'OCMockito', '~> 7.0'
end

Carthage

将以下内容添加到您的 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