Skip to content

Commit 6095ebf

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 4343dc4 commit 6095ebf

File tree

8 files changed

+339
-20
lines changed

8 files changed

+339
-20
lines changed

Source/OCMock.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@
279279
817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; };
280280
817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; };
281281
817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; };
282+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
283+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
282284
8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; };
283285
8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; };
284286
8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; };
@@ -569,6 +571,7 @@
569571
3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestClassWithCustomReferenceCounting.h; sourceTree = "<group>"; };
570572
3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestClassWithCustomReferenceCounting.m; sourceTree = "<group>"; };
571573
817EB1621BD765130047E85A /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
574+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectCleanupTests.m; sourceTree = "<group>"; };
572575
8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
573576
A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = "<group>"; };
574577
D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -736,6 +739,7 @@
736739
03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */,
737740
039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */,
738741
2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */,
742+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */,
739743
2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */,
740744
03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */,
741745
0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */,
@@ -1497,6 +1501,7 @@
14971501
03565A4218F05721003AE91E /* OCMockObjectPartialMocksTests.m in Sources */,
14981502
03565A4C18F05721003AE91E /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
14991503
03565A4818F05721003AE91E /* OCMStubRecorderTests.m in Sources */,
1504+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */,
15001505
03565A4518F05721003AE91E /* OCMockObjectForwardingTargetTests.m in Sources */,
15011506
2FA28FA53C57236B6DD64E82 /* OCMockObjectRuntimeTests.m in Sources */,
15021507
2FA2839F33289795284C32FB /* OCMockObjectTests.m in Sources */,
@@ -1609,6 +1614,7 @@
16091614
D31108CA1828DBD600737925 /* NSInvocationOCMAdditionsTests.m in Sources */,
16101615
03C9CA1F18F05A8E006DF94D /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
16111616
03C9CA1D18F05A75006DF94D /* OCMockObjectProtocolMocksTests.m in Sources */,
1617+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */,
16121618
03E98D5118F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */,
16131619
A06930951CA1BFC900513023 /* TestObjects.xcdatamodeld in Sources */,
16141620
2FA28295E1F58F40A77D7448 /* OCMockObjectRuntimeTests.m in Sources */,

Source/OCMock/OCClassMockObject.m

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,18 @@ @implementation OCClassMockObject
2727

2828
- (id)initWithClass:(Class)aClass
2929
{
30-
NSParameterAssert(aClass != nil);
31-
[super init];
32-
mockedClass = aClass;
33-
[self prepareClassForClassMethodMocking];
30+
@try
31+
{
32+
NSParameterAssert(aClass != nil);
33+
[super init];
34+
mockedClass = aClass;
35+
[self prepareClassForClassMethodMocking];
36+
}
37+
@catch(NSException *e)
38+
{
39+
[OCMockObject removeAMockToStop:self];
40+
[e raise];
41+
}
3442
return self;
3543
}
3644

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

@@ -44,7 +44,7 @@ - (void)handleInvocation:(NSInvocation *)anInvocation
4444
if(matchAndReject)
4545
{
4646
isSatisfied = NO;
47-
[NSException raise:NSInternalInconsistencyException format:@"%@: explicitly disallowed method invoked: %@",
47+
[OCMockObject logMatcherIssue:@"%@: explicitly disallowed method invoked: %@",
4848
[self description], [anInvocation invocationDescription]];
4949
}
5050
else

Source/OCMock/OCMockObject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,8 @@
7272
- (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location;
7373
- (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location;
7474

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

Source/OCMock/OCMockObject.m

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,59 @@
3131
#import "OCMExpectationRecorder.h"
3232
#import "OCMQuantifier.h"
3333

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

3546
@implementation OCMockObject
3647

3748
#pragma mark Class initialisation
3849

3950
+ (void)initialize
4051
{
41-
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
42-
[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?"];
52+
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
53+
{
54+
[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?"];
55+
}
4356
}
4457

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

4688
#pragma mark Factory methods
4789

@@ -109,6 +151,7 @@ - (instancetype)init
109151
expectations = [[NSMutableArray alloc] init];
110152
exceptions = [[NSMutableArray alloc] init];
111153
invocations = [[NSMutableArray alloc] init];
154+
[OCMockObject recordAMockToStop:self];
112155
return self;
113156
}
114157

@@ -144,11 +187,22 @@ - (void)addExpectation:(OCMInvocationExpectation *)anExpectation
144187

145188
- (void)assertInvocationsArrayIsPresent
146189
{
147-
if(invocations == nil) {
148-
[NSException raise:NSInternalInconsistencyException format:@"** Cannot handle or verify invocations on %@ 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], self];
190+
if(invocations == nil)
191+
{
192+
[OCMockObject logMatcherIssue:@"** Cannot handle or verify invocations on %@ 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], self];
149193
}
150194
}
151195

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+
}
152206

153207
#pragma mark Public API
154208

@@ -448,7 +502,7 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
448502
{
449503
if(isNice == NO)
450504
{
451-
[NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@",
505+
[OCMockObject logMatcherIssue:@"%@: unexpected method invoked: %@ %@",
452506
[self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]];
453507
}
454508
}
@@ -508,4 +562,83 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
508562
}
509563

510564

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

Source/OCMock/OCPartialMockObject.m

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

3030
- (id)initWithObject:(NSObject *)anObject
3131
{
32-
NSParameterAssert(anObject != nil);
33-
Class const class = [self classToSubclassForObject:anObject];
34-
[self assertClassIsSupported:class];
35-
[super initWithClass:class];
36-
realObject = [anObject retain];
37-
[self prepareObjectForInstanceMethodMocking];
32+
@try
33+
{
34+
NSParameterAssert(anObject != nil);
35+
Class const class = [self classToSubclassForObject:anObject];
36+
[self assertClassIsSupported:class];
37+
[super initWithClass:class];
38+
realObject = [anObject retain];
39+
[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: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,17 @@ @implementation OCProtocolMockObject
2424

2525
- (id)initWithProtocol:(Protocol *)aProtocol
2626
{
27-
NSParameterAssert(aProtocol != nil);
28-
[super init];
29-
mockedProtocol = aProtocol;
27+
@try
28+
{
29+
NSParameterAssert(aProtocol != nil);
30+
[super init];
31+
mockedProtocol = aProtocol;
32+
}
33+
@catch(NSException *e)
34+
{
35+
[OCMockObject removeAMockToStop:self];
36+
[e raise];
37+
}
3038
return self;
3139
}
3240

0 commit comments

Comments
 (0)