blob: 5c885fdd94ff20a9b74f3ea3145c1fb0fb3a6b34 [file] [log] [blame]
/*
* Copyright (c) 2014-2021 Erik Doernenburg and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use these files except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
#import <XCTest/XCTest.h>
#import "OCMock.h"
@protocol TestProtocolForMacroTesting
- (NSString *)stringValue;
@end
@interface TestClassForMacroTesting : NSObject<TestProtocolForMacroTesting>
@end
@implementation TestClassForMacroTesting
- (NSString *)stringValue
{
return @"FOO";
}
@end
@interface TestClassWithDecimalReturnMethod : NSObject
- (NSDecimalNumber *)method;
@end
@implementation TestClassWithDecimalReturnMethod
- (NSDecimalNumber *)method
{
return nil;
}
@end
@interface TestClassWithClassReturnMethod : NSObject
- (Class)method;
@end
@implementation TestClassWithClassReturnMethod
- (Class)method
{
return [self class];
}
@end
@interface TestClassWithLazyMock : NSObject
- (id)mock;
@end
@implementation TestClassWithLazyMock
{
id mock;
}
- (id)mock
{
if(mock == nil)
mock = OCMClassMock([NSString class]);
return mock;
}
@end
// implemented in OCMockObjectClassMethodMockingTests
@interface TestClassWithClassMethods : NSObject
+ (NSString *)foo;
+ (NSString *)bar;
- (NSString *)bar;
@end
@interface OCMockObjectMacroTests : XCTestCase
{
BOOL shouldCaptureFailure;
NSString *reportedDescription;
NSString *reportedFile;
NSInteger reportedLine;
}
@end
@implementation OCMockObjectMacroTests
#if defined(__IPHONE_14_0) && !defined(OCM_DISABLE_XCTEST_FEATURES) // this is actually a test for Xcode 12; see issue #472
- (void)recordIssue:(XCTIssue *)issue
{
if(shouldCaptureFailure)
{
reportedDescription = issue.compactDescription;
reportedFile = issue.sourceCodeContext.location.fileURL.path;
reportedLine = issue.sourceCodeContext.location.lineNumber;
}
else
{
[super recordIssue:issue];
}
}
#else
- (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)file atLine:(NSUInteger)line expected:(BOOL)expected
{
if(shouldCaptureFailure)
{
reportedDescription = description;
reportedFile = file;
reportedLine = line;
}
else
{
[super recordFailureWithDescription:description inFile:file atLine:line expected:expected];
}
}
#endif
- (void)testReportsVerifyFailureWithCorrectLocation
{
id mock = OCMClassMock([NSString class]);
[[mock expect] lowercaseString];
shouldCaptureFailure = YES;
OCMVerifyAll(mock); const char *expectedFile = __FILE__; int expectedLine = __LINE__;
shouldCaptureFailure = NO;
XCTAssertNotNil(reportedDescription, @"Should have recorded a failure with description.");
XCTAssertTrue([reportedFile hasSuffix:[NSString stringWithUTF8String:expectedFile]], @"Should have reported correct file.");
XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line");
}
- (void)testReportsIgnoredExceptionsAtVerifyLocation
{
id mock = OCMClassMock([NSString class]);
[[mock reject] lowercaseString];
@try
{
[mock lowercaseString];
}
@catch(NSException *exception)
{
// ignore; the mock will rethrow this in verify
}
shouldCaptureFailure = YES;
OCMVerifyAll(mock); const char *expectedFile = __FILE__; int expectedLine = __LINE__;
shouldCaptureFailure = NO;
XCTAssertTrue([reportedDescription rangeOfString:@"ignored"].location != NSNotFound, @"Should have reported ignored exceptions.");
XCTAssertTrue([reportedFile hasSuffix:[NSString stringWithUTF8String:expectedFile]], @"Should have reported correct file.");
XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line");
}
- (void)testReportsVerifyWithDelayFailureWithCorrectLocation
{
id mock = OCMClassMock([NSString class]);
[[mock expect] lowercaseString];
shouldCaptureFailure = YES;
OCMVerifyAllWithDelay(mock, 0.05); const char *expectedFile = __FILE__; int expectedLine = __LINE__;
shouldCaptureFailure = NO;
XCTAssertNotNil(reportedDescription, @"Should have recorded a failure with description.");
XCTAssertTrue([reportedFile hasSuffix:[NSString stringWithUTF8String:expectedFile]], @"Should have reported correct file.");
XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line");
}
- (void)testSetsUpStubsForCorrectMethods
{
id mock = OCMStrictClassMock([NSString class]);
OCMStub([mock uppercaseString]).andReturn(@"TEST_STRING");
XCTAssertEqualObjects(@"TEST_STRING", [mock uppercaseString], @"Should have returned stubbed value");
XCTAssertThrows([mock lowercaseString]);
}
- (void)testSetsUpStubsWithNonObjectReturnValues
{
id mock = OCMStrictClassMock([NSString class]);
OCMStub([mock boolValue]).andReturn(YES);
XCTAssertEqual(YES, [mock boolValue], @"Should have returned stubbed value");
}
- (void)testSetsUpStubsWithStructureReturnValues
{
id mock = OCMStrictClassMock([NSString class]);
NSRange expected = NSMakeRange(123, 456);
OCMStub([mock rangeOfString:[OCMArg any]]).andReturn(expected);
NSRange actual = [mock rangeOfString:@"substring"];
XCTAssertEqual((NSUInteger)123, actual.location, @"Should have returned stubbed value");
XCTAssertEqual((NSUInteger)456, actual.length, @"Should have returned stubbed value");
}
- (void)testSetsUpStubReturningNilForIdReturnType
{
id mock = OCMPartialMock([NSArray arrayWithObject:@"Foo"]);
OCMStub([mock lastObject]).andReturn(nil);
XCTAssertNil([mock lastObject], @"Should have returned stubbed value");
}
- (void)testSetsUpStubReturningNilForClassReturnType
{
id mock = OCMPartialMock([[TestClassWithClassReturnMethod alloc] init]);
OCMStub([mock method]).andReturn(Nil);
XCTAssertNil([mock method], @"Should have returned stubbed value");
// sometimes nil is used where Nil should be used
OCMStub([mock method]).andReturn(nil);
XCTAssertNil([mock method], @"Should have returned stubbed value");
}
- (void)testSetsUpExceptionThrowing
{
id mock = OCMClassMock([NSString class]);
OCMStub([mock uppercaseString]).andThrow([NSException exceptionWithName:@"TestException" reason:@"Testing" userInfo:nil]);
XCTAssertThrowsSpecificNamed([mock uppercaseString], NSException, @"TestException", @"Should have thrown correct exception");
}
- (void)testSetsUpNotificationPostingAndNotificationObserving
{
id mock = OCMProtocolMock(@protocol(TestProtocolForMacroTesting));
NSNotification *n = [NSNotification notificationWithName:@"TestNotification" object:nil];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
id observer = OCMObserverMock();
#pragma clang diagnostic pop
[[NSNotificationCenter defaultCenter] addMockObserver:observer name:[n name] object:nil];
OCMExpect([observer notificationWithName:[n name] object:[OCMArg any]]);
OCMStub([mock stringValue]).andPost(n);
[mock stringValue];
OCMVerifyAll(observer);
}
- (void)testNotificationObservingWithUserInfo
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
id observer = OCMObserverMock();
#pragma clang diagnostic pop
[[NSNotificationCenter defaultCenter] addMockObserver:observer name:@"TestNotificationWithInfo" object:nil];
OCMExpect([observer notificationWithName:@"TestNotificationWithInfo" object:[OCMArg any] userInfo:[OCMArg any]]);
[[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotificationWithInfo" object:self userInfo:@{ @"foo" : @"bar" }];
OCMVerifyAll(observer);
}
- (void)testSetsUpSubstituteCall
{
id mock = OCMStrictProtocolMock(@protocol(TestProtocolForMacroTesting));
OCMStub([mock stringValue]).andCall(self, @selector(stringValueForTesting));
XCTAssertEqualObjects([mock stringValue], @"TEST_STRING_FROM_TESTCASE", @"Should have called method from test case");
}
- (NSString *)stringValueForTesting
{
return @"TEST_STRING_FROM_TESTCASE";
}
#ifndef OCM_DISABLE_XCTEST_FEATURES
- (void)testFulfillsExpectation
{
id mock = OCMStrictClassMock([NSString class]);
OCMStub([mock boolValue]).andFulfill([self expectationWithDescription:@"Expectation Called"]).andReturn(YES);
XCTAssertTrue([mock boolValue]);
[self waitForExpectationsWithTimeout:0 handler:nil];
}
#endif
- (void)testCanChainPropertyBasedActions
{
id mock = OCMPartialMock([[TestClassForMacroTesting alloc] init]);
__block BOOL didCallBlock = NO;
void (^theBlock)(NSInvocation *) = ^(NSInvocation *invocation) {
didCallBlock = YES;
};
OCMStub([mock stringValue]).andDo(theBlock).andForwardToRealObject();
NSString *actual = [mock stringValue];
XCTAssertTrue(didCallBlock, @"Should have called block");
XCTAssertEqualObjects(@"FOO", actual, @"Should have forwarded invocation");
}
- (void)testCanUseVariablesInInvocationSpec
{
id mock = OCMStrictClassMock([NSString class]);
NSString *expected = @"foo";
OCMStub([mock rangeOfString:expected]).andReturn(NSMakeRange(0, 3));
XCTAssertThrows([mock rangeOfString:@"bar"], @"Should not have accepted invocation with non-matching arg.");
}
- (void)testSetsUpExpectations
{
id mock = OCMClassMock([TestClassForMacroTesting class]);
OCMExpect([mock stringValue]).andReturn(@"TEST_STRING");
XCTAssertThrows([mock verify], @"Should have complained about expected method not being invoked");
XCTAssertEqual([mock stringValue], @"TEST_STRING", @"Should have stubbed method, too");
XCTAssertNoThrow([mock verify], @"Should have accepted invocation as matching expectation");
}
- (void)testSetsUpReject
{
id mock = OCMClassMock([TestClassForMacroTesting class]);
OCMReject([mock stringValue]);
XCTAssertNoThrow([mock verify], @"Should have accepted invocation rejected method not being invoked");
XCTAssertThrows([mock stringValue], @"Should have complained during rejected method being invoked");
XCTAssertThrows([mock verify], @"Should have complained about rejected method being invoked");
}
- (void)testThrowsWhenTryingToAddActionToReject
{
id mock = OCMClassMock([TestClassForMacroTesting class]);
XCTAssertThrows(OCMReject([mock stringValue]).andReturn(@"Foo"));
}
- (void)testShouldNotReportErrorWhenMethodWasInvoked
{
id mock = OCMClassMock([NSString class]);
[mock lowercaseString];
shouldCaptureFailure = YES;
OCMVerify([mock lowercaseString]);
shouldCaptureFailure = NO;
XCTAssertNil(reportedDescription, @"Should not have recorded a failure.");
}
- (void)testShouldReportErrorWhenMethodWasNotInvoked
{
id mock = OCMClassMock([NSString class]);
[mock lowercaseString];
shouldCaptureFailure = YES;
OCMVerify([mock uppercaseString]); const char *expectedFile = __FILE__; int expectedLine = __LINE__;
shouldCaptureFailure = NO;
XCTAssertNotNil(reportedDescription, @"Should have recorded a failure with description.");
XCTAssertTrue([reportedFile hasSuffix:[NSString stringWithUTF8String:expectedFile]], @"Should have reported correct file.");
XCTAssertEqual(expectedLine, (int)reportedLine, @"Should have reported correct line");
}
- (void)testShouldThrowDescriptiveExceptionWhenTryingToVerifyUnimplementedMethod
{
id mock = OCMClassMock([NSString class]);
// have not found a way to report the error; it seems we must throw an
// exception to get out of the forwarding machinery
XCTAssertThrowsSpecificNamed(OCMVerify([mock arrayByAddingObject:@"foo"]),
NSException, NSInvalidArgumentException, @"should throw NSInvalidArgumentException exception");
}
- (void)testShouldThrowExceptionWhenNotUsingMockInMacroThatRequiresMock
{
id realObject = [NSMutableArray array];
XCTAssertThrowsSpecificNamed(OCMStub([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException);
XCTAssertThrowsSpecificNamed(OCMExpect([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException);
XCTAssertThrowsSpecificNamed(OCMReject([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException);
XCTAssertThrowsSpecificNamed(OCMVerify([realObject addObject:@"foo"]), NSException, NSInternalInconsistencyException);
}
- (void)testShouldHintAtPossibleReasonWhenNotUsingMockInMacroThatRequiresMock
{
@try
{
id realObject = [NSMutableArray array];
OCMStub([realObject addObject:@"foo"]);
}
@catch(NSException *e)
{
XCTAssertTrue([[e reason] containsString:@"The receiver is not a mock object."]);
}
}
- (void)testShouldThrowExceptionWhenMockingMethodThatCannotBeMocked
{
id mock = OCMClassMock([NSString class]);
XCTAssertThrowsSpecificNamed(OCMStub([mock description]), NSException, NSInternalInconsistencyException);
XCTAssertThrowsSpecificNamed(OCMExpect([mock description]), NSException, NSInternalInconsistencyException);
XCTAssertThrowsSpecificNamed(OCMReject([mock description]), NSException, NSInternalInconsistencyException);
XCTAssertThrowsSpecificNamed(OCMVerify([mock description]), NSException, NSInternalInconsistencyException);
}
- (void)testShouldHintAtPossibleReasonWhenMockingMethodThatCannotBeMocked
{
@try
{
id mock = OCMClassMock([NSString class]);
OCMStub([mock description]);
}
@catch(NSException *e)
{
XCTAssertTrue([[e reason] containsString:@"The selector conflicts with a selector implemented by OCMStubRecorder/OCMExpectationRecorder."]);
}
}
- (void)testShouldHintAtPossibleReasonWhenVerifyingMethodThatCannotBeMocked
{
@try
{
id mock = OCMClassMock([NSString class]);
OCMVerify([mock description]);
}
@catch(NSException *e)
{
XCTAssertTrue([[e reason] containsString:@"The selector conflicts with a selector implemented by OCMVerifier."]);
}
}
- (void)testCanExplicitlySelectClassMethodForStubs
{
id mock = OCMClassMock([TestClassWithClassMethods class]);
OCMStub(ClassMethod([mock bar])).andReturn(@"mocked-class");
OCMStub([mock bar]).andReturn(@"mocked-instance");
XCTAssertEqualObjects(@"mocked-class", [TestClassWithClassMethods bar], @"Should have stubbed class method.");
XCTAssertEqualObjects(@"mocked-instance", [mock bar], @"Should have stubbed instance method.");
}
- (void)testSelectsInstanceMethodForStubsWhenAmbiguous
{
id mock = OCMClassMock([TestClassWithClassMethods class]);
OCMStub([mock bar]).andReturn(@"mocked-instance");
XCTAssertEqualObjects(@"mocked-instance", [mock bar], @"Should have stubbed instance method.");
}
- (void)testSelectsClassMethodForStubsWhenUnambiguous
{
id mock = OCMClassMock([TestClassWithClassMethods class]);
OCMStub([mock foo]).andReturn(@"mocked-class");
XCTAssertEqualObjects(@"mocked-class", [TestClassWithClassMethods foo], @"Should have stubbed class method.");
}
- (void)testCanExplicitlySelectClassMethodForVerify
{
id mock = OCMClassMock([TestClassWithClassMethods class]);
[TestClassWithClassMethods bar];
OCMVerify(ClassMethod([mock bar]));
}
- (void)testSelectsInstanceMethodForVerifyWhenAmbiguous
{
id mock = OCMClassMock([TestClassWithClassMethods class]);
[mock bar];
OCMVerify([mock bar]);
}
- (void)testSelectsClassMethodForVerifyWhenUnambiguous
{
id mock = OCMClassMock([TestClassWithClassMethods class]);
[TestClassWithClassMethods foo];
OCMVerify([mock foo]);
}
- (void)testCanUseMacroToStubMethodWithDecimalReturnValue
{
id mock = OCMClassMock([TestClassWithDecimalReturnMethod class]);
OCMStub([mock method]).andReturn([NSDecimalNumber decimalNumberWithDecimal:[@0 decimalValue]]);
XCTAssertEqualObjects([mock method], [NSDecimalNumber decimalNumberWithDecimal:[@0 decimalValue]]);
}
- (void)testCanUseMacroToStubMethodWithAnyNonObjectArgument
{
id mock = OCMStrictClassMock([NSString class]);
OCMStub([mock commonPrefixWithString:@"foo" options:0]).ignoringNonObjectArgs();
XCTAssertNoThrow([mock commonPrefixWithString:@"foo" options:NSCaseInsensitiveSearch]);
}
- (void)testCanUseMacroToStubMethodWithAnyNonObjectArgumentChainedWithOCMStubRecorder
{
id mock = OCMClassMock([NSString class]);
OCMStub([mock commonPrefixWithString:@"foo" options:0]).ignoringNonObjectArgs().andReturn(@"f");
XCTAssertEqualObjects(@"f", [mock commonPrefixWithString:@"foo" options:NSCaseInsensitiveSearch]);
}
- (void)testReturnsCorrectObjectFromInitMethodCalledOnRecorderInsideMacro
{
// Because of the way the macros work, you can call recorder methods on the mock and they will
// work correctly. Technically these are a mix of old syntax and new.
//
// There are no assertions here, the tests will crash with an incorrect implementation.
//
// Note that the andReturn:nil has to be first because this is the stub that will actually be
// used and we're now making sure that a return value is specified for init methods.
id mock = OCMClassMock([NSString class]);
OCMStub([[mock andReturn:nil] initWithString:OCMOCK_ANY]);
OCMStub([[mock ignoringNonObjectArgs] initWithString:OCMOCK_ANY]);
OCMStub([[mock andReturnValue:nil] initWithString:OCMOCK_ANY]);
OCMStub([[mock andThrow:nil] initWithString:OCMOCK_ANY]);
OCMStub([[mock andPost:nil] initWithString:OCMOCK_ANY]);
OCMStub([[mock andCall:nil onObject:nil] initWithString:OCMOCK_ANY]);
#ifndef OCM_DISABLE_XCTEST_FEATURES
OCMStub([[mock andFulfill:nil] initWithString:OCMOCK_ANY]);
#endif
OCMStub([[mock andDo:nil] initWithString:OCMOCK_ANY]);
OCMStub([[mock andForwardToRealObject] initWithString:OCMOCK_ANY]);
OCMExpect([[mock never] initWithString:OCMOCK_ANY]);
__unused id value = [mock initWithString:@"hello"];
_OCMVerify([(id)[mock withQuantifier:nil] initWithString:OCMOCK_ANY]);
// Test multiple levels of recorder methods.
OCMStub([[[[mock ignoringNonObjectArgs] andReturn:nil] andThrow:nil] initWithString:OCMOCK_ANY]);
}
- (void)testStubMacroPassesExceptionThrough
{
id mock = OCMClassMock([TestClassForMacroTesting class]);
@try
{
OCMStub([mock init]).andReturn(mock);
XCTFail(@"An exception should have been thrown.");
}
@catch(NSException *exception)
{
XCTAssertEqualObjects(exception.name, NSInternalInconsistencyException);
XCTAssertTrue([exception.reason containsString:@"Method init invoked twice on stub recorder"]);
}
}
- (void)testExpectMacroPassesExceptionThrough
{
id mock = OCMClassMock([TestClassForMacroTesting class]);
@try
{
OCMExpect([mock init]).andReturn(mock);
XCTFail(@"An exception should have been thrown.");
}
@catch(NSException *exception)
{
XCTAssertEqualObjects(exception.name, NSInternalInconsistencyException);
XCTAssertTrue([exception.reason containsString:@"Method init invoked twice on stub recorder"]);
}
}
- (void)testVerifyMacroPassExceptionsThrough
{
id mock = OCMClassMock([TestClassForMacroTesting class]);
@try
{
// The -Wunused-value is a workaround for https://bugs.llvm.org/show_bug.cgi?id=45245
_Pragma("clang diagnostic push")
_Pragma("clang diagnostic ignored \"-Wunused-value\"")
OCMVerify([mock init]);
_Pragma("clang diagnostic pop")
XCTFail(@"An exception should have been thrown.");
}
@catch(NSException *exception)
{
XCTAssertEqualObjects(exception.name, NSInternalInconsistencyException);
XCTAssertTrue([exception.reason containsString:@"Method init invoked twice on verifier"]);
}
}
- (void)testMockGeneratedLazily
{
TestClassWithLazyMock *lazyMock = [[TestClassWithLazyMock alloc] init];
XCTAssertNoThrow(OCMStub([[lazyMock mock] lowercaseString]).andReturn(@"bar"));
}
@end