Skip to content

Commit b52f0b6

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 b8746e5 commit b52f0b6

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
@@ -73,5 +73,8 @@
7373
- (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location;
7474
- (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count;
7575

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

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

@@ -146,10 +189,20 @@ - (void)assertInvocationsArrayIsPresent
146189
{
147190
if(invocations == nil)
148191
{
149-
[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];
192+
[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];
150193
}
151194
}
152195

196+
+ (void)logMatcherIssue:(NSString *)format, ...
197+
{
198+
if(gAssertOnCallsAfterStopMocking)
199+
{
200+
va_list args;
201+
va_start(args, format);
202+
[NSException raise:NSInternalInconsistencyException format:format arguments:args];
203+
va_end(args);
204+
}
205+
}
153206

154207
#pragma mark Public API
155208

@@ -459,7 +512,7 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
459512
{
460513
if(isNice == NO)
461514
{
462-
[NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@",
515+
[OCMockObject logMatcherIssue:@"%@: unexpected method invoked: %@ %@",
463516
[self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]];
464517
}
465518
}
@@ -519,4 +572,83 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
519572
}
520573

521574

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