Skip to content

Commit 0b74e05

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 0b74e05

File tree

8 files changed

+343
-28
lines changed

8 files changed

+343
-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: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,59 @@
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;
3344

3445
@implementation OCMockObject
3546

3647
#pragma mark Class initialisation
3748

3849
+ (void)initialize
3950
{
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?"];
51+
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
52+
{
53+
[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+
}
4255
}
4356

57+
#pragma mark Mock cleanup recording
58+
59+
+ (void)recordAMockToStop:(OCMockObject *)mock
60+
{
61+
@synchronized(self)
62+
{
63+
[[gMocksToStopRecorders lastObject] addObject:mock];
64+
}
65+
}
66+
67+
+ (void)removeAMockToStop:(OCMockObject *)mock
68+
{
69+
@synchronized(self)
70+
{
71+
[[gMocksToStopRecorders lastObject] removeObject:mock];
72+
}
73+
}
74+
75+
+ (void)stopAllCurrentMocks
76+
{
77+
@synchronized(self) {
78+
NSHashTable<OCMockObject *> *recorder = [gMocksToStopRecorders lastObject];
79+
for (OCMockObject *mock in recorder)
80+
{
81+
[mock stopMocking];
82+
}
83+
[recorder removeAllObjects];
84+
}
85+
}
4486

4587
#pragma mark Factory methods
4688

@@ -108,6 +150,7 @@ - (instancetype)init
108150
expectations = [[NSMutableArray alloc] init];
109151
exceptions = [[NSMutableArray alloc] init];
110152
invocations = [[NSMutableArray alloc] init];
153+
[OCMockObject recordAMockToStop:self];
111154
return self;
112155
}
113156

@@ -157,7 +200,7 @@ - (void)assertInvocationsArrayIsPresent
157200
{
158201
if(invocations == nil)
159202
{
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];
203+
[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];
161204
}
162205
}
163206

@@ -176,6 +219,16 @@ - (void)addInvocation:(NSInvocation *)anInvocation
176219
}
177220
}
178221

222+
+ (void)logMatcherIssue:(NSString *)format, ...
223+
{
224+
if(gAssertOnCallsAfterStopMocking)
225+
{
226+
va_list args;
227+
va_start(args, format);
228+
[NSException raise:NSInternalInconsistencyException format:format arguments:args];
229+
va_end(args);
230+
}
231+
}
179232

180233
#pragma mark Public API
181234

@@ -465,7 +518,7 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
465518
{
466519
if(isNice == NO)
467520
{
468-
[NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@",
521+
[OCMockObject logMatcherIssue:@"%@: unexpected method invoked: %@ %@",
469522
[self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]];
470523
}
471524
}
@@ -525,4 +578,83 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
525578
}
526579

527580

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