Skip to content

Commit 7a67922

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 ffeeed2 commit 7a67922

File tree

8 files changed

+356
-28
lines changed

8 files changed

+356
-28
lines changed

Source/OCMock.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@
285285
8B11D4BB2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */; settings = {COMPILER_FLAGS = "-std=gnu++11"; }; };
286286
8BF73E53246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */; settings = {COMPILER_FLAGS = "-Xclang -fexperimental-optimized-noescape"; }; };
287287
8BF73E54246CA75E00B9A52C /* OCMNoEscapeBlockTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */; settings = {COMPILER_FLAGS = "-Xclang -fexperimental-optimized-noescape"; }; };
288+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
289+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
288290
8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; };
289291
8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; };
290292
8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; };
@@ -578,6 +580,7 @@
578580
8B11D4B62448E2E900247BE2 /* OCMCPlusPlus98Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OCMCPlusPlus98Tests.mm; sourceTree = "<group>"; };
579581
8B11D4B92448E53600247BE2 /* OCMCPlusPlus11Tests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OCMCPlusPlus11Tests.mm; sourceTree = "<group>"; };
580582
8BF73E52246CA75E00B9A52C /* OCMNoEscapeBlockTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMNoEscapeBlockTests.m; sourceTree = "<group>"; };
583+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectCleanupTests.m; sourceTree = "<group>"; };
581584
8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
582585
A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = "<group>"; };
583586
D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -745,6 +748,7 @@
745748
03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */,
746749
039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */,
747750
2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */,
751+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */,
748752
2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */,
749753
03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */,
750754
0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */,
@@ -1507,6 +1511,7 @@
15071511
03565A4218F05721003AE91E /* OCMockObjectPartialMocksTests.m in Sources */,
15081512
03565A4C18F05721003AE91E /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
15091513
03565A4818F05721003AE91E /* OCMStubRecorderTests.m in Sources */,
1514+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */,
15101515
03565A4518F05721003AE91E /* OCMockObjectForwardingTargetTests.m in Sources */,
15111516
2FA28FA53C57236B6DD64E82 /* OCMockObjectRuntimeTests.m in Sources */,
15121517
8B11D4BA2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */,
@@ -1622,6 +1627,7 @@
16221627
D31108CA1828DBD600737925 /* NSInvocationOCMAdditionsTests.m in Sources */,
16231628
03C9CA1F18F05A8E006DF94D /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
16241629
03C9CA1D18F05A75006DF94D /* OCMockObjectProtocolMocksTests.m in Sources */,
1630+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */,
16251631
03E98D5118F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */,
16261632
A06930951CA1BFC900513023 /* TestObjects.xcdatamodeld in Sources */,
16271633
8B11D4BB2448E53600247BE2 /* OCMCPlusPlus11Tests.mm in Sources */,

Source/OCMock/OCClassMockObject.m

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,20 @@ @implementation OCClassMockObject
2727

2828
- (id)initWithClass:(Class)aClass
2929
{
30-
if(aClass == Nil)
31-
[NSException raise:NSInvalidArgumentException format:@"Class cannot be Nil."];
32-
33-
[super init];
34-
mockedClass = aClass;
35-
[self prepareClassForClassMethodMocking];
36-
return self;
30+
@try
31+
{
32+
if(aClass == Nil)
33+
[NSException raise:NSInvalidArgumentException format:@"Class cannot be Nil."];
34+
[super init];
35+
mockedClass = aClass;
36+
[self prepareClassForClassMethodMocking];
37+
}
38+
@catch(NSException *e)
39+
{
40+
[OCMockObject removeAMockToStop:self];
41+
[e raise];
42+
}
43+
return self;
3744
}
3845

3946
- (void)dealloc

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
@@ -30,18 +30,73 @@
3030
#import "OCMExceptionReturnValueProvider.h"
3131
#import "OCMExpectationRecorder.h"
3232

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

3448
@implementation OCMockObject
3549

3650
#pragma mark Class initialisation
3751

3852
+ (void)initialize
3953
{
40-
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
41-
[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?"];
54+
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
55+
{
56+
[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?"];
57+
}
58+
}
59+
60+
#pragma mark Mock cleanup recording
61+
62+
+ (void)recordAMockToStop:(OCMockObject *)mock
63+
{
64+
@synchronized(self)
65+
{
66+
if(gStoppingMocks)
67+
{
68+
[NSException raise:NSInternalInconsistencyException format:@"Attempting to add a mock while mocks are being stopped."];
69+
}
70+
[[gMocksToStopRecorders lastObject] addObject:mock];
71+
}
4272
}
4373

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

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

47102
+ (id)mockForClass:(Class)aClass
@@ -108,6 +163,7 @@ - (instancetype)init
108163
expectations = [[NSMutableArray alloc] init];
109164
exceptions = [[NSMutableArray alloc] init];
110165
invocations = [[NSMutableArray alloc] init];
166+
[OCMockObject recordAMockToStop:self];
111167
return self;
112168
}
113169

@@ -157,7 +213,7 @@ - (void)assertInvocationsArrayIsPresent
157213
{
158214
if(invocations == nil)
159215
{
160-
[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];
216+
[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];
161217
}
162218
}
163219

@@ -176,6 +232,16 @@ - (void)addInvocation:(NSInvocation *)anInvocation
176232
}
177233
}
178234

235+
+ (void)logMatcherIssue:(NSString *)format, ...
236+
{
237+
if(gAssertOnCallsAfterStopMocking)
238+
{
239+
va_list args;
240+
va_start(args, format);
241+
[NSException raise:NSInternalInconsistencyException format:format arguments:args];
242+
va_end(args);
243+
}
244+
}
179245

180246
#pragma mark Public API
181247

@@ -465,7 +531,7 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
465531
{
466532
if(isNice == NO)
467533
{
468-
[NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@",
534+
[OCMockObject logMatcherIssue:@"%@: unexpected method invoked: %@ %@",
469535
[self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]];
470536
}
471537
}
@@ -525,4 +591,83 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
525591
}
526592

527593

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

Source/OCMock/OCPartialMockObject.m

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,22 @@ @implementation OCPartialMockObject
3030

3131
- (id)initWithObject:(NSObject *)anObject
3232
{
33-
if(anObject == nil)
34-
[NSException raise:NSInvalidArgumentException format:@"Object cannot be nil."];
35-
36-
Class const class = [self classToSubclassForObject:anObject];
37-
[self assertClassIsSupported:class];
38-
[super initWithClass:class];
39-
realObject = [anObject retain];
40-
[self prepareObjectForInstanceMethodMocking];
41-
return self;
33+
@try
34+
{
35+
if(anObject == nil)
36+
[NSException raise:NSInvalidArgumentException format:@"Object cannot be nil."];
37+
Class const class = [self classToSubclassForObject:anObject];
38+
[self assertClassIsSupported:class];
39+
[super initWithClass:class];
40+
realObject = [anObject retain];
41+
[self prepareObjectForInstanceMethodMocking];
42+
}
43+
@catch(NSException *e)
44+
{
45+
[OCMockObject removeAMockToStop:self];
46+
[e raise];
47+
}
48+
return self;
4249
}
4350

4451
- (NSString *)description

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)