OCMock:invokeBlockWithArgs 与 checkWithBlock

OCMock: invokeBlockWithArgs vs checkWithBlock

我正在阅读 OCMock reference,我对这两个 OCMArg 方法 invokeBlockWithArgs(第 2.6 节)感到困惑

The mock object will invoke the block passed as an argument to the stubbed method. If the block takes arguments and invokeBlock is used, the default values for the argument types are used, e.g. zero for a numerical type. Using invokeBlockWithArgs: it is possible to specify which arguments to invoke the block with; non-object arguments must be wrapped in value objects and the expression must be wrapped in round brackets.

和 checkWithBlock(第 4.3 节)

For checkWithSelector:onObject:, when the mock object receives someMethod:, it invokes aSelector on anObject. If the method takes an argument the mock will pass the argument that was passed to someMethod:. The method should return a boolean indicating whether the argument matched the expectation or not.

因此,使用 checkWithBlock 我们可以使用我们提供的任何参数调用传递的块,使用 invokeBlockWithArgs 似乎也可以这样做。那么什么时候使用第一种或者第二种方法呢?

checkWithBlock - 您提供一个块,该块将被调用以断言传递给存根方法的值,您与 [OCMArg checkWithBlock:] 交换的值符合您的期望。

invokeBlockWithArgs - 这可以在存根块参数时使用,以使用示例参数调用它们。如果您想检查块的行为,这是必需的。


假设我们有一个简单的 Networking 客户端,只有一个方法:

- (void)call:(NSURL *)url completion: (^void(NSData *, NSError *));

我们还有一些 ModelClass 将我们的 Networking 的实例作为 init 中的依赖项,看起来像这样:

@property (nonatomic, nullable, strong) NSData *lastData;
@property (nonatomic, nullable, strong) NSError *lastError;

@property (nonatomic, strong) Networking *networking;

- (instancetype)initWith:(Networking *)networking { /* */ }

- (void)getData {
    [self.networking call:[NSURL URLWithString:@"www.whosebug.com"]
               completion: ^(NSData *newData, NSError *newError) {
                             self.lastData = newData;
                             self.lastError = newError;
               }];
}

然后我们可以在我们的测试中像这样测试 getData 方法 class :

@property (nonatomic, strong) Networking *networkingMock;
@property (nonatomic, strong) ModelClass *model;

- (void)setUp {
    [super setUp];

    self.networkingMock = OCMClassMock([Networking class]);
    self.model = [[ModelClass alloc] initWith:self.networkingMock];
}

// Assert proper argument was passed by explicitly providing
// expected value in `OCMStub`/`OCMExpect` call
// OCMock will check that they are equal for us
- (void)test_getData_passesCorrectURL {

    // Arrange
    OCMExpect([self.networkingMock call:[NSURL URLWithString:@"www.whosebug.com"] 
                             completion:OCMOCK_ANY]);

    // Act
    [self.model getData];

    // Assert
    OCMVerifyAll(self.networkingMock);
}

// Assert proper argument is passed in a custom assertion block
// OCMock will call this block, passing the value so that we can inspect it
// We cannot use `invokeBlockWithArgs` to check the `url` parameter
// because its not a block.
- (void)test_getData_passesCorrectURL_withCheckWithBlock {
    // Arrange
    OCMExpect([self.networkingMock call:[OCMArg checkWithBlock:^BOOL(id value) {
                             // This is the custom assertion block, we can inspect the `value` here
                             // We need to return `YES`/`NO` depending if it matches our expectetations

                             if (![value isKindOfClass:[NSURL class]]) { return NO };
                             NSURL *valueAsURL = (NSURL *)value;
                             return [valueAsURL isEqualToURL:[NSURL URLWithString:@"www.whosebug.com"]];
                             }] 
                             completion:OCMOCK_ANY]);

    // Act
    [self.model getData];

    // Assert
    OCMVerifyAll(self.networkingMock);
}

// We want to assert the behavior of the completion block passed to the `Networking`
// in the `getData` method. So we need a way to invoke this block somehow - 
// in previous two tests it was never called, because `OCMock` replaces the 
// implementations of methods in stubbed classes.
- (void)test_getData_shouldSetLastData_onCompletion {

    // Arrange
    NSData *expectedData = [NSData data];
    OCMExpect([self.networkingMock call:OCMOCK_ANY
                             completion:[OCMArg invokeBlockWithArgs:expectedData, [NSNull null], nil]]);

    // Act
    [self.model getData];

    // Assert
    XCTAssertEqualObjects(self.model.lastData, expectedData);

}

在上一个示例中,如果您使用 checkWithBlock 而不是 invokeBlockWithArgs,则不会调用传入 ModelClass 的完成块。相反,自定义断言块将被调用(正如我们在第二个测试中看到的那样)并且指向完成块的指针将作为值传递。

您当然可以将此指针转换为块类型,并使用一些参数自己调用块 - 但由于 invokeBlockWithArgs.

,这是可以避免的额外工作

注意: invokeBlockWithArgs 采用 var_args 参数列表,需要用 nil 终止以指示结束。我们需要使用 [NSNull null] 来表示我们希望将 nil 作为某个参数传递给我们的完成块 - 因此在上面的示例中,我们的完成块将使用 expectedData 作为 newData,和 nil(来自 [NSNull null])作为 newError