Skip to content

Commit 584002a

Browse files
committed
Automatically stop all live mocks at the end of each test case/suite
If the user is using XCTest with OCMock, this registers a test observer that takes care of stopping all live mocks appropriately. For mocks that are created in +setUp, those will get stopped at the end of the suite. For mocks that are created in -setUp or in test cases themselves, those will get stopped at the end of the testcase. While these mocks are being stopped and testcases/suites are being torndown, messages sent to mocks are not going to trigger the exception about calling a mock after it has had stopMocking called on it. This allows objects that may refer to mocks in dealloc methods to be cleaned up in autoreleasepools or due to stopMocking being called without the mocks throwing exceptions. This should greatly simplify cleaning up mocks and remove a lot of potential leakage. It also makes sure that class mocks that mock class methods will not persist across tests.
1 parent c81c481 commit 584002a

File tree

8 files changed

+348
-18
lines changed

8 files changed

+348
-18
lines changed

Source/OCMock.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@
294294
8BF7402124772B0600B9A52C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03565A1D18F05626003AE91E /* XCTest.framework */; };
295295
8BF7402224772B0800B9A52C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03565A1D18F05626003AE91E /* XCTest.framework */; };
296296
8BF7402324772B0800B9A52C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03565A1D18F05626003AE91E /* XCTest.framework */; };
297+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
298+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
297299
8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; };
298300
8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; };
299301
8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; };
@@ -589,6 +591,7 @@
589591
8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OCMCPlusPlus11Tests.mm; sourceTree = "<group>"; };
590592
8B3786A724E5BD5600FD1B5B /* OCMFunctionsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OCMFunctionsTests.m; sourceTree = "<group>"; };
591593
8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNoEscapeBlockTests.m; sourceTree = "<group>"; };
594+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectCleanupTests.m; sourceTree = "<group>"; };
592595
8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
593596
A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = "<group>"; };
594597
D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -755,6 +758,7 @@
755758
03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */,
756759
039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */,
757760
2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */,
761+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */,
758762
2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */,
759763
03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */,
760764
0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */,
@@ -1536,6 +1540,7 @@
15361540
03565A4218F05721003AE91E /* OCMockObjectPartialMocksTests.m in Sources */,
15371541
03565A4C18F05721003AE91E /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
15381542
03565A4818F05721003AE91E /* OCMStubRecorderTests.m in Sources */,
1543+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */,
15391544
03565A4518F05721003AE91E /* OCMockObjectForwardingTargetTests.m in Sources */,
15401545
2FA28FA53C57236B6DD64E82 /* OCMockObjectRuntimeTests.m in Sources */,
15411546
8B11D4BA2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */,
@@ -1652,6 +1657,7 @@
16521657
D31108CA1828DBD600737925 /* NSInvocationOCMAdditionsTests.m in Sources */,
16531658
03C9CA1F18F05A8E006DF94D /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
16541659
03C9CA1D18F05A75006DF94D /* OCMockObjectProtocolMocksTests.m in Sources */,
1660+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */,
16551661
03E98D5118F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */,
16561662
A06930951CA1BFC900513023 /* TestObjects.xcdatamodeld in Sources */,
16571663
8B11D4BB2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */,

Source/OCMock/OCClassMockObject.m

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,18 @@ @implementation OCClassMockObject
3232

3333
- (id)initWithClass:(Class)aClass
3434
{
35-
[self assertClassIsSupported:aClass];
36-
[super init];
37-
mockedClass = aClass;
35+
@try
36+
{
37+
[self assertClassIsSupported:aClass];
38+
[super init];
39+
mockedClass = aClass;
3840
[self prepareClassForClassMethodMocking];
41+
}
42+
@catch(NSException *e)
43+
{
44+
[OCMockObject removeAMockToStop:self];
45+
[e raise];
46+
}
3947
return self;
4048
}
4149

Source/OCMock/OCMInvocationExpectation.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
#import "OCMInvocationExpectation.h"
1818
#import "NSInvocation+OCMAdditions.h"
19-
19+
#import "OCMockObject.h"
2020

2121
@implementation OCMInvocationExpectation
2222

@@ -52,7 +52,7 @@ - (void)handleInvocation:(NSInvocation *)anInvocation
5252
if(matchAndReject)
5353
{
5454
isSatisfied = NO;
55-
[NSException raise:NSInternalInconsistencyException format:@"%@: explicitly disallowed method invoked: %@",
55+
[OCMockObject logMatcherIssue:@"%@: explicitly disallowed method invoked: %@",
5656
[self description], [anInvocation invocationDescription]];
5757
}
5858
else

Source/OCMock/OCMockObject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,8 @@
7474
- (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location;
7575
- (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count;
7676

77+
+ (void)removeAMockToStop:(OCMockObject *)mock;
78+
79+
+ (void)logMatcherIssue:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
7780
@end
7881

Source/OCMock/OCMockObject.m

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,73 @@
2929
#import "OCMFunctionsPrivate.h"
3030
#import "NSInvocation+OCMAdditions.h"
3131

32+
@class XCTestCase;
33+
@class XCTest;
34+
35+
// gMocksToStopRecorders is a stack of recorders that gets added to and removed from
36+
// as we enter test suite/case scopes.
37+
// Controlled by OCMockXCTestObserver.
38+
static NSMutableArray<NSHashTable<OCMockObject *> *> *gMocksToStopRecorders;
39+
40+
// Flag that controls whether we should be asserting after stopmocking is called.
41+
// Controlled by OCMockXCTestObserver.
42+
static BOOL gAssertOnCallsAfterStopMocking;
43+
44+
// Flag that tracks if we are stopping the mocks.
45+
static BOOL gStoppingMocks = NO;
3246

3347
@implementation OCMockObject
3448

3549
#pragma mark Class initialisation
3650

3751
+ (void)initialize
3852
{
39-
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
40-
[NSException raise:NSInternalInconsistencyException format:@"** Expected method not present; the method getArgumentAtIndexAsObject: is not implemented by NSInvocation. If you see this exception it is likely that you are using the static library version of OCMock and your project is not configured correctly to load categories from static libraries. Did you forget to add the -ObjC linker flag?"];
53+
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
54+
{
55+
[NSException raise:NSInternalInconsistencyException format:@"** Expected method not present; the method getArgumentAtIndexAsObject: is not implemented by NSInvocation. If you see this exception it is likely that you are using the static library version of OCMock and your project is not configured correctly to load categories from static libraries. Did you forget to add the -ObjC linker flag?"];
56+
}
57+
}
58+
59+
#pragma mark Mock cleanup recording
60+
61+
+ (void)recordAMockToStop:(OCMockObject *)mock
62+
{
63+
@synchronized(self)
64+
{
65+
if(gStoppingMocks)
66+
{
67+
[NSException raise:NSInternalInconsistencyException format:@"Attempting to add a mock while mocks are being stopped."];
68+
}
69+
[[gMocksToStopRecorders lastObject] addObject:mock];
70+
}
4171
}
4272

73+
+ (void)removeAMockToStop:(OCMockObject *)mock
74+
{
75+
@synchronized(self)
76+
{
77+
if(gStoppingMocks)
78+
{
79+
[NSException raise:NSInternalInconsistencyException format:@"Attempting to remove a mock while mocks are being stopped."];
80+
}
81+
[[gMocksToStopRecorders lastObject] removeObject:mock];
82+
}
83+
}
4384

85+
+ (void)stopAllCurrentMocks
86+
{
87+
@synchronized(self)
88+
{
89+
gStoppingMocks = YES;
90+
NSHashTable<OCMockObject *> *recorder = [gMocksToStopRecorders lastObject];
91+
for (OCMockObject *mock in recorder)
92+
{
93+
[mock stopMocking];
94+
}
95+
[recorder removeAllObjects];
96+
gStoppingMocks = NO;
97+
}
98+
}
4499
#pragma mark Factory methods
45100

46101
+ (id)mockForClass:(Class)aClass
@@ -112,6 +167,7 @@ - (instancetype)init
112167
expectations = [[NSMutableArray alloc] init];
113168
exceptions = [[NSMutableArray alloc] init];
114169
invocations = [[NSMutableArray alloc] init];
170+
[OCMockObject recordAMockToStop:self];
115171
return self;
116172
}
117173

@@ -161,7 +217,7 @@ - (void)assertInvocationsArrayIsPresent
161217
{
162218
if(invocations == nil)
163219
{
164-
[NSException raise:NSInternalInconsistencyException format:@"** Cannot use mock object %@ at %p. This error usually occurs when a mock object is used after stopMocking has been called on it. In most cases it is not necessary to call stopMocking. If you know you have to, please make sure that the mock object is not used afterwards.", [self description], (void *)self];
220+
[OCMockObject logMatcherIssue:@"** Cannot use mock object %@ at %p. This error usually occurs when a mock object is used after stopMocking has been called on it. In most cases it is not necessary to call stopMocking. If you know you have to, please make sure that the mock object is not used afterwards.", [self description], (void *)self];
165221
}
166222
}
167223

@@ -180,6 +236,16 @@ - (void)addInvocation:(NSInvocation *)anInvocation
180236
}
181237
}
182238

239+
+ (void)logMatcherIssue:(NSString *)format, ...
240+
{
241+
if(gAssertOnCallsAfterStopMocking)
242+
{
243+
va_list args;
244+
va_start(args, format);
245+
[NSException raise:NSInternalInconsistencyException format:format arguments:args];
246+
va_end(args);
247+
}
248+
}
183249

184250
#pragma mark Public API
185251

@@ -469,7 +535,7 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
469535
{
470536
if(isNice == NO)
471537
{
472-
[NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@",
538+
[OCMockObject logMatcherIssue:@"%@: unexpected method invoked: %@ %@",
473539
[self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]];
474540
}
475541
}
@@ -529,4 +595,83 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
529595
}
530596

531597

598+
@end
599+
600+
/**
601+
* The observer gets installed the first time a mock object is created (see +[OCMockObject initialize]
602+
* It stops all the mocks that are still active when the testcase has finished.
603+
* In many cases this should break a lot of retain loops and allow mocks to be freed.
604+
* More importantly this will remove mocks that have mocked a class method and persist across testcases.
605+
* It intentionally turns off the assert that fires when calling a mock after stopMocking has been
606+
* called on it, because when we are doing cleanup there are cases in dealloc methods where a mock
607+
* may be called. We allow the "assert off" state to persist beyond the end of -testCaseDidFinish
608+
* because objects may be destroyed by the autoreleasepool that wraps the entire test and this may
609+
* cause mocks to be called. The state is global (instead of per mock) because we want to be able
610+
* to catch the case where a mock is trapped by some global state (e.g. a non-mock singleton) and
611+
* then that singleton is used in a later test and attempts to call a stopped mock.
612+
**/
613+
@interface OCMockXCTestObserver : NSObject
614+
@end
615+
616+
// "Fake" Protocol so we can avoid having to link to XCTest, but not get warnings about
617+
// methods not being declared.
618+
@protocol OCMockXCTestObservation
619+
+ (id)sharedTestObservationCenter;
620+
- (void)addTestObserver:(id)observer;
621+
@end
622+
623+
@implementation OCMockXCTestObserver
624+
625+
+ (void)load
626+
{
627+
gMocksToStopRecorders = [[NSMutableArray alloc] init];
628+
gAssertOnCallsAfterStopMocking = YES;
629+
Class xctest = NSClassFromString(@"XCTestObservationCenter");
630+
if (xctest)
631+
{
632+
// If XCTest is available, we set up an observer to stop our mocks for us.
633+
[[xctest sharedTestObservationCenter] addTestObserver:[[OCMockXCTestObserver alloc] init]];
634+
}
635+
}
636+
637+
- (BOOL)conformsToProtocol:(Protocol *)aProtocol
638+
{
639+
// This allows us to avoid linking XCTest into OCMock.
640+
return strcmp(protocol_getName(aProtocol), "XCTestObservation") == 0;
641+
}
642+
643+
- (void)addRecorder
644+
{
645+
gAssertOnCallsAfterStopMocking = YES;
646+
NSHashTable<OCMockObject *> *recorder = [NSHashTable weakObjectsHashTable];
647+
[gMocksToStopRecorders addObject:recorder];
648+
}
649+
650+
- (void)finalizeRecorder
651+
{
652+
gAssertOnCallsAfterStopMocking = NO;
653+
[OCMockObject stopAllCurrentMocks];
654+
[gMocksToStopRecorders removeLastObject];
655+
}
656+
657+
- (void)testSuiteWillStart:(XCTestCase *)testCase
658+
{
659+
[self addRecorder];
660+
}
661+
662+
- (void)testSuiteDidFinish:(XCTestCase *)testCase
663+
{
664+
[self finalizeRecorder];
665+
}
666+
667+
- (void)testCaseWillStart:(XCTestCase *)testCase
668+
{
669+
[self addRecorder];
670+
}
671+
672+
- (void)testCaseDidFinish:(XCTestCase *)testCase
673+
{
674+
[self finalizeRecorder];
675+
}
676+
532677
@end

Source/OCMock/OCPartialMockObject.m

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,20 @@ @implementation OCPartialMockObject
2929

3030
- (id)initWithObject:(NSObject *)anObject
3131
{
32+
@try
33+
{
3234
if(anObject == nil)
3335
[NSException raise:NSInvalidArgumentException format:@"Object cannot be nil."];
34-
Class const class = [self classToSubclassForObject:anObject];
35-
[super initWithClass:class];
36-
realObject = [anObject retain];
36+
Class const class = [self classToSubclassForObject:anObject];
37+
[super initWithClass:class];
38+
realObject = [anObject retain];
3739
[self prepareObjectForInstanceMethodMocking];
40+
}
41+
@catch(NSException *e)
42+
{
43+
[OCMockObject removeAMockToStop:self];
44+
[e raise];
45+
}
3846
return self;
3947
}
4048

Source/OCMock/OCProtocolMockObject.m

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,19 @@ @implementation OCProtocolMockObject
2424

2525
- (id)initWithProtocol:(Protocol *)aProtocol
2626
{
27-
if(aProtocol == nil)
28-
[NSException raise:NSInvalidArgumentException format:@"Protocol cannot be nil."];
29-
30-
[super init];
31-
mockedProtocol = aProtocol;
32-
return self;
27+
@try
28+
{
29+
if(aProtocol == nil)
30+
[NSException raise:NSInvalidArgumentException format:@"Protocol cannot be nil."];
31+
[super init];
32+
mockedProtocol = aProtocol;
33+
}
34+
@catch(NSException *e)
35+
{
36+
[OCMockObject removeAMockToStop:self];
37+
[e raise];
38+
}
39+
return self;
3340
}
3441

3542
- (NSString *)description

0 commit comments

Comments
 (0)