Skip to content

Commit 06ecc97

Browse files
committed
Automatically stop all live mocks at the end of each test case
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 042fd19 commit 06ecc97

File tree

7 files changed

+244
-17
lines changed

7 files changed

+244
-17
lines changed

Source/OCMock.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,8 @@
280280
817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; };
281281
817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; };
282282
817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; };
283+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
284+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
283285
8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; };
284286
8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; };
285287
8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; };
@@ -570,6 +572,7 @@
570572
3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestClassWithCustomReferenceCounting.h; sourceTree = "<group>"; };
571573
3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestClassWithCustomReferenceCounting.m; sourceTree = "<group>"; };
572574
817EB1621BD765130047E85A /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
575+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectCleanupTests.m; sourceTree = "<group>"; };
573576
8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
574577
A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = "<group>"; };
575578
D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -737,6 +740,7 @@
737740
03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */,
738741
039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */,
739742
2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */,
743+
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */,
740744
2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */,
741745
03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */,
742746
0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */,
@@ -1500,6 +1504,7 @@
15001504
03565A4218F05721003AE91E /* OCMockObjectPartialMocksTests.m in Sources */,
15011505
03565A4C18F05721003AE91E /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
15021506
03565A4818F05721003AE91E /* OCMStubRecorderTests.m in Sources */,
1507+
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */,
15031508
03565A4518F05721003AE91E /* OCMockObjectForwardingTargetTests.m in Sources */,
15041509
2FA28FA53C57236B6DD64E82 /* OCMockObjectRuntimeTests.m in Sources */,
15051510
2FA2839F33289795284C32FB /* OCMockObjectTests.m in Sources */,
@@ -1612,6 +1617,7 @@
16121617
D31108CA1828DBD600737925 /* NSInvocationOCMAdditionsTests.m in Sources */,
16131618
03C9CA1F18F05A8E006DF94D /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
16141619
03C9CA1D18F05A75006DF94D /* OCMockObjectProtocolMocksTests.m in Sources */,
1620+
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */,
16151621
03E98D5118F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */,
16161622
A06930951CA1BFC900513023 /* TestObjects.xcdatamodeld in Sources */,
16171623
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/OCMockObject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,6 @@
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;
7576
@end
7677

Source/OCMock/OCMockObject.m

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,73 @@
3131
#import "OCMExpectationRecorder.h"
3232
#import "OCMQuantifier.h"
3333

34+
@class XCTestCase;
3435

35-
@implementation OCMockObject
36+
static NSHashTable<OCMockObject *> *gTestCaseMocksToStop;
37+
static NSHashTable<OCMockObject *> *gTestSuiteMocksToStop;
38+
static NSHashTable<OCMockObject *> *gCurrentMocksToStopRecorder;
39+
40+
static BOOL gAssertOnCallsAfterStopMocking;
41+
42+
@protocol OCMockXCTestObservation
43+
+ (id)sharedTestObservationCenter;
44+
- (void)addTestObserver:(id)observer;
45+
@end
46+
47+
@interface OCMockXCTestObserver : NSObject
48+
@end
3649

50+
@implementation OCMockObject
3751
#pragma mark Class initialisation
52+
+ (void)load
53+
{
54+
gTestCaseMocksToStop = [[NSHashTable weakObjectsHashTable] retain];
55+
gTestSuiteMocksToStop = [[NSHashTable weakObjectsHashTable] retain];
56+
gCurrentMocksToStopRecorder = gTestSuiteMocksToStop;
57+
gAssertOnCallsAfterStopMocking = YES;
58+
Class xctest = NSClassFromString(@"XCTestObservationCenter");
59+
if (xctest)
60+
{
61+
// If XCTest is available, we set up an observer to stop our mocks for us.
62+
[[xctest sharedTestObservationCenter] addTestObserver:[[OCMockXCTestObserver alloc] init]];
63+
}
64+
65+
}
3866

3967
+ (void)initialize
4068
{
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?"];
69+
if (self == [OCMockObject class]) {
70+
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
71+
{
72+
[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?"];
73+
}
74+
}
75+
}
76+
77+
#pragma mark Mock cleanup recording
78+
79+
+ (void)recordAMockToStop:(OCMockObject *)mock {
80+
[gCurrentMocksToStopRecorder addObject:mock];
81+
}
82+
83+
+ (void)removeAMockToStop:(OCMockObject *)mock {
84+
[gCurrentMocksToStopRecorder removeObject:mock];
85+
}
86+
87+
+ (void)stopAllTestCaseMocks {
88+
for (OCMockObject *mock in gTestCaseMocksToStop)
89+
{
90+
[mock stopMocking];
91+
}
92+
[gTestCaseMocksToStop removeAllObjects];
93+
}
94+
95+
+ (void)stopAllTestSuiteMocks {
96+
for (OCMockObject *mock in gTestSuiteMocksToStop)
97+
{
98+
[mock stopMocking];
99+
}
100+
[gTestSuiteMocksToStop removeAllObjects];
43101
}
44102

45103

@@ -109,6 +167,7 @@ - (instancetype)init
109167
expectations = [[NSMutableArray alloc] init];
110168
exceptions = [[NSMutableArray alloc] init];
111169
invocations = [[NSMutableArray alloc] init];
170+
[OCMockObject recordAMockToStop:self];
112171
return self;
113172
}
114173

@@ -144,7 +203,7 @@ - (void)addExpectation:(OCMInvocationExpectation *)anExpectation
144203

145204
- (void)assertInvocationsArrayIsPresent
146205
{
147-
if(invocations == nil) {
206+
if(gAssertOnCallsAfterStopMocking && invocations == nil) {
148207
[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];
149208
}
150209
}
@@ -508,4 +567,46 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
508567
}
509568

510569

570+
@end
571+
572+
/**
573+
* The observer gets installed the first time a mock object is created (see +[OCMockObject initialize]
574+
* It stops all the mocks that are still active when the testcase has finished.
575+
* In many cases this should break a lot of retain loops and allow mocks to be freed.
576+
* More importantly this will remove mocks that have mocked a class method and persist across testcases.
577+
* It intentionally turns off the assert that fires when calling a mock after stopMocking has been
578+
* called on it, because when we are doing cleanup there are cases in dealloc methods where a mock
579+
* may be called. We allow the "assert off" state to persist beyond the end of -testCaseDidFinish
580+
* because objects may be destroyed by the autoreleasepool that wraps the entire test and this may
581+
* cause mocks to be called. The state is global (instead of per mock) because we want to be able
582+
* to catch the case where a mock is trapped by some global state (e.g. a non-mock singleton) and
583+
* then that singleton is used in a later test and attempts to call a stopped mock.
584+
**/
585+
@implementation OCMockXCTestObserver
586+
587+
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
588+
// This allows us to avoid linking XCTest into OCMock.
589+
return strcmp(protocol_getName(aProtocol), "XCTestObservation") == 0;
590+
}
591+
592+
- (void)testSuiteWillStart:(XCTestCase *)testCase {
593+
gAssertOnCallsAfterStopMocking = YES;
594+
gCurrentMocksToStopRecorder = gTestSuiteMocksToStop;
595+
}
596+
597+
- (void)testSuiteDidFinish:(XCTestCase *)testCase {
598+
gAssertOnCallsAfterStopMocking = NO;
599+
[OCMockObject stopAllTestSuiteMocks];
600+
}
601+
602+
- (void)testCaseWillStart:(XCTestCase *)testCase {
603+
gAssertOnCallsAfterStopMocking = YES;
604+
gCurrentMocksToStopRecorder = gTestCaseMocksToStop;
605+
}
606+
607+
- (void)testCaseDidFinish:(XCTestCase *)testCase {
608+
gAssertOnCallsAfterStopMocking = NO;
609+
[OCMockObject stopAllTestCaseMocks];
610+
}
611+
511612
@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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright (c) 2015-2020 Erik Doernenburg and contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may
5+
* not use these files except in compliance with the License. You may obtain
6+
* a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
#import <XCTest/XCTest.h>
18+
#import <OCMock/OCMock.h>
19+
20+
#pragma mark Helper classes
21+
22+
// Tests for mocks being stopped by the XCTestObserver that we register in OCMockObject.
23+
@interface OCMockObjectCleanupTests : XCTestCase
24+
@end
25+
26+
@implementation OCMockObjectCleanupTests
27+
28+
29+
static id suiteMock;
30+
static id caseMock;
31+
32+
+ (void)setUp
33+
{
34+
suiteMock = [OCMockObject mockForClass:[NSString class]];
35+
OCMStub([suiteMock intValue]).andReturn(42);
36+
caseMock = nil;
37+
}
38+
39+
#pragma mark Tests suite mocks survive across test cases
40+
41+
- (void)testSuiteMockWorksHere
42+
{
43+
// Verify that a testSuite Mock made in +setUp doesn't get cleaned up until test suite is done.
44+
// By verifying in two test cases we know this is true (See testSuiteMockWorksAndHere).
45+
XCTAssertEqual([suiteMock intValue], 42);
46+
}
47+
48+
- (void)testSuiteMockWorksAndHere
49+
{
50+
// Verify that a testSuite Mock made in +setUp doesn't get cleaned up until test suite is done.
51+
// By verifying in two test cases we know this is true (See testSuiteMockWorksHere).
52+
XCTAssertEqual([suiteMock intValue], 42);
53+
}
54+
55+
#pragma mark Tests case mocks get stopped across test cases
56+
57+
- (void)setUpCaseMock
58+
{
59+
caseMock = [OCMockObject mockForClass:[NSString class]];
60+
}
61+
62+
- (void)testCaseMockFailsEitherHere
63+
{
64+
// Set up a mock here that should get cleaned up (but the global pointer will still be non-nil)
65+
// or test that the mock set up in testCaseMockFailsOrHere has had stop mocking called on it.
66+
if (!caseMock)
67+
{
68+
[self setUpCaseMock];
69+
}
70+
else
71+
{
72+
XCTAssertThrows([caseMock stringValue],
73+
@"Expected a throw here because the caseMock set up in "
74+
@"testCaseMockFailsOrHere should have had stopMock called on it");
75+
}
76+
}
77+
78+
- (void)testCaseMockFailsOrHere
79+
{
80+
// Set up a mock here that should get cleaned up (but the global pointer will still be non-nil)
81+
// or test that the mock set up in testCaseMockFailsOrHere has had stop mocking called on it.
82+
if (!caseMock)
83+
{
84+
[self setUpCaseMock];
85+
}
86+
else
87+
{
88+
XCTAssertThrows([caseMock stringValue],
89+
@"Expected a throw here because the caseMock set up in "
90+
@"testCaseMockFailsEitherHere should have had stopMock called on it");
91+
}
92+
}
93+
94+
95+
@end

0 commit comments

Comments
 (0)