Skip to content

Commit

Permalink
Automatically stop all live mocks at the end of each test case/suite
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dmaclach committed Mar 26, 2020
1 parent 042fd19 commit 56dd248
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 17 deletions.
6 changes: 6 additions & 0 deletions Source/OCMock.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@
817EB15C1BD765130047E85A /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2891034E7B73AA3511D17 /* OCMBlockArgCaller.h */; };
817EB15D1BD765130047E85A /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 2FA2833B48908EAD36444671 /* OCMArgAction.h */; };
817EB1661BD7674D0047E85A /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F370CA1BAA1DE800CAD3E8 /* OCMFunctionsPrivate.h */; };
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */; };
8DE97C5522B43EE60098C63F /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3159E146333BF0052CD09 /* OCMockObject.m */; };
8DE97C5622B43EE60098C63F /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B3158C146333BF0052CD09 /* OCClassMockObject.m */; };
8DE97C5722B43EE60098C63F /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 03B315AA146333BF0052CD09 /* OCPartialMockObject.m */; };
Expand Down Expand Up @@ -570,6 +572,7 @@
3CFBDD751BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestClassWithCustomReferenceCounting.h; sourceTree = "<group>"; };
3CFBDD761BB3DB200050D9C5 /* TestClassWithCustomReferenceCounting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TestClassWithCustomReferenceCounting.m; sourceTree = "<group>"; };
817EB1621BD765130047E85A /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OCMockObjectCleanupTests.m; sourceTree = "<group>"; };
8DE97CA022B43EE60098C63F /* OCMock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; };
A02926811CA0725A00594AAF /* TestObjects.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TestObjects.xcdatamodel; sourceTree = "<group>"; };
D31108AD1828DB8700737925 /* OCMockLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OCMockLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -737,6 +740,7 @@
03AC5C1416DF9FA500D82ECD /* OCMockObjectPartialMocksTests.m */,
039F91C516EFB493006C3D70 /* OCMockObjectClassMethodMockingTests.m */,
2FA286BFBD8B9D068B41E7EF /* OCMockObjectProtocolMocksTests.m */,
8BC0A67B242D08D800695F71 /* OCMockObjectCleanupTests.m */,
2FA28EE3142412BD601026EF /* OCMockObjectDynamicPropertyMockingTests.m */,
03E98D4F18F310EE00522D42 /* OCMockObjectMacroTests.m */,
0354D71F16F23AF5001766BB /* OCMockObjectForwardingTargetTests.m */,
Expand Down Expand Up @@ -1500,6 +1504,7 @@
03565A4218F05721003AE91E /* OCMockObjectPartialMocksTests.m in Sources */,
03565A4C18F05721003AE91E /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
03565A4818F05721003AE91E /* OCMStubRecorderTests.m in Sources */,
8BC0A67C242D08D800695F71 /* OCMockObjectCleanupTests.m in Sources */,
03565A4518F05721003AE91E /* OCMockObjectForwardingTargetTests.m in Sources */,
2FA28FA53C57236B6DD64E82 /* OCMockObjectRuntimeTests.m in Sources */,
2FA2839F33289795284C32FB /* OCMockObjectTests.m in Sources */,
Expand Down Expand Up @@ -1612,6 +1617,7 @@
D31108CA1828DBD600737925 /* NSInvocationOCMAdditionsTests.m in Sources */,
03C9CA1F18F05A8E006DF94D /* NSMethodSignatureOCMAdditionsTests.m in Sources */,
03C9CA1D18F05A75006DF94D /* OCMockObjectProtocolMocksTests.m in Sources */,
8BC0A67D242D08E400695F71 /* OCMockObjectCleanupTests.m in Sources */,
03E98D5118F310EE00522D42 /* OCMockObjectMacroTests.m in Sources */,
A06930951CA1BFC900513023 /* TestObjects.xcdatamodeld in Sources */,
2FA28295E1F58F40A77D7448 /* OCMockObjectRuntimeTests.m in Sources */,
Expand Down
16 changes: 12 additions & 4 deletions Source/OCMock/OCClassMockObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,18 @@ @implementation OCClassMockObject

- (id)initWithClass:(Class)aClass
{
NSParameterAssert(aClass != nil);
[super init];
mockedClass = aClass;
[self prepareClassForClassMethodMocking];
@try
{
NSParameterAssert(aClass != nil);
[super init];
mockedClass = aClass;
[self prepareClassForClassMethodMocking];
}
@catch(NSException *e)
{
[OCMockObject removeAMockToStop:self];
[e raise];
}
return self;
}

Expand Down
1 change: 1 addition & 0 deletions Source/OCMock/OCMockObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@
- (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location;
- (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location;

+ (void)removeAMockToStop:(OCMockObject *)mock;
@end

116 changes: 112 additions & 4 deletions Source/OCMock/OCMockObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,80 @@
#import "OCMExpectationRecorder.h"
#import "OCMQuantifier.h"

@class XCTestCase;

// gCurrentMocksToStopRecorder will point to either gTestCaseMocksToStop or gTestSuiteMocksToStop
// depending on whether we are currently recoring testcase mocks or test suite mocks.
// Controlled by OCMockXCTestObserver.
static NSHashTable<OCMockObject *> *gTestCaseMocksToStop;
static NSHashTable<OCMockObject *> *gTestSuiteMocksToStop;
static NSHashTable<OCMockObject *> *gCurrentMocksToStopRecorder;

// Flag that controls whether we should be asserting after stopmocking is called.
// Controlled by OCMockXCTestObserver.
static BOOL gAssertOnCallsAfterStopMocking;

// "Fake" Protocol so we can avoid having to link to XCTest, but not get warnings about
// methods not being declared.
@protocol OCMockXCTestObservation
+ (id)sharedTestObservationCenter;
- (void)addTestObserver:(id)observer;
@end

@implementation OCMockObject
@interface OCMockXCTestObserver : NSObject
@end

@implementation OCMockObject
#pragma mark Class initialisation
+ (void)load
{
gTestCaseMocksToStop = [[NSHashTable weakObjectsHashTable] retain];
gTestSuiteMocksToStop = [[NSHashTable weakObjectsHashTable] retain];
gCurrentMocksToStopRecorder = nil;
gAssertOnCallsAfterStopMocking = YES;
Class xctest = NSClassFromString(@"XCTestObservationCenter");
if (xctest)
{
// If XCTest is available, we set up an observer to stop our mocks for us.
[[xctest sharedTestObservationCenter] addTestObserver:[[OCMockXCTestObserver alloc] init]];
}

}

+ (void)initialize
{
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
[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?"];
if (self == [OCMockObject class]) {
if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL)
{
[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?"];
}
}
}

#pragma mark Mock cleanup recording

+ (void)recordAMockToStop:(OCMockObject *)mock {
[gCurrentMocksToStopRecorder addObject:mock];
}

+ (void)removeAMockToStop:(OCMockObject *)mock {
[gCurrentMocksToStopRecorder removeObject:mock];
}

+ (void)stopAllTestCaseMocks {
for (OCMockObject *mock in gTestCaseMocksToStop)
{
[mock stopMocking];
}
[gTestCaseMocksToStop removeAllObjects];
}

+ (void)stopAllTestSuiteMocks {
for (OCMockObject *mock in gTestSuiteMocksToStop)
{
[mock stopMocking];
}
[gTestSuiteMocksToStop removeAllObjects];
}


Expand Down Expand Up @@ -109,6 +174,7 @@ - (instancetype)init
expectations = [[NSMutableArray alloc] init];
exceptions = [[NSMutableArray alloc] init];
invocations = [[NSMutableArray alloc] init];
[OCMockObject recordAMockToStop:self];
return self;
}

Expand Down Expand Up @@ -144,7 +210,7 @@ - (void)addExpectation:(OCMInvocationExpectation *)anExpectation

- (void)assertInvocationsArrayIsPresent
{
if(invocations == nil) {
if(gAssertOnCallsAfterStopMocking && invocations == nil) {
[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];
}
}
Expand Down Expand Up @@ -508,4 +574,46 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
}


@end

/**
* The observer gets installed the first time a mock object is created (see +[OCMockObject initialize]
* It stops all the mocks that are still active when the testcase has finished.
* In many cases this should break a lot of retain loops and allow mocks to be freed.
* More importantly this will remove mocks that have mocked a class method and persist across testcases.
* It intentionally turns off the assert that fires when calling a mock after stopMocking has been
* called on it, because when we are doing cleanup there are cases in dealloc methods where a mock
* may be called. We allow the "assert off" state to persist beyond the end of -testCaseDidFinish
* because objects may be destroyed by the autoreleasepool that wraps the entire test and this may
* cause mocks to be called. The state is global (instead of per mock) because we want to be able
* to catch the case where a mock is trapped by some global state (e.g. a non-mock singleton) and
* then that singleton is used in a later test and attempts to call a stopped mock.
**/
@implementation OCMockXCTestObserver

- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
// This allows us to avoid linking XCTest into OCMock.
return strcmp(protocol_getName(aProtocol), "XCTestObservation") == 0;
}

- (void)testSuiteWillStart:(XCTestCase *)testCase {
gAssertOnCallsAfterStopMocking = YES;
gCurrentMocksToStopRecorder = gTestSuiteMocksToStop;
}

- (void)testSuiteDidFinish:(XCTestCase *)testCase {
gAssertOnCallsAfterStopMocking = NO;
[OCMockObject stopAllTestSuiteMocks];
}

- (void)testCaseWillStart:(XCTestCase *)testCase {
gAssertOnCallsAfterStopMocking = YES;
gCurrentMocksToStopRecorder = gTestCaseMocksToStop;
}

- (void)testCaseDidFinish:(XCTestCase *)testCase {
gAssertOnCallsAfterStopMocking = NO;
[OCMockObject stopAllTestCaseMocks];
}

@end
20 changes: 14 additions & 6 deletions Source/OCMock/OCPartialMockObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,20 @@ @implementation OCPartialMockObject

- (id)initWithObject:(NSObject *)anObject
{
NSParameterAssert(anObject != nil);
Class const class = [self classToSubclassForObject:anObject];
[self assertClassIsSupported:class];
[super initWithClass:class];
realObject = [anObject retain];
[self prepareObjectForInstanceMethodMocking];
@try
{
NSParameterAssert(anObject != nil);
Class const class = [self classToSubclassForObject:anObject];
[self assertClassIsSupported:class];
[super initWithClass:class];
realObject = [anObject retain];
[self prepareObjectForInstanceMethodMocking];
}
@catch(NSException *e)
{
[OCMockObject removeAMockToStop:self];
[e raise];
}
return self;
}

Expand Down
14 changes: 11 additions & 3 deletions Source/OCMock/OCProtocolMockObject.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ @implementation OCProtocolMockObject

- (id)initWithProtocol:(Protocol *)aProtocol
{
NSParameterAssert(aProtocol != nil);
[super init];
mockedProtocol = aProtocol;
@try
{
NSParameterAssert(aProtocol != nil);
[super init];
mockedProtocol = aProtocol;
}
@catch(NSException *e)
{
[OCMockObject removeAMockToStop:self];
[e raise];
}
return self;
}

Expand Down
95 changes: 95 additions & 0 deletions Source/OCMockTests/OCMockObjectCleanupTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2015-2020 Erik Doernenburg and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use these files except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>

#pragma mark Helper classes

// Tests for mocks being stopped by the XCTestObserver that we register in OCMockObject.
@interface OCMockObjectCleanupTests : XCTestCase
@end

@implementation OCMockObjectCleanupTests


static id suiteMock;
static id caseMock;

+ (void)setUp
{
suiteMock = [OCMockObject mockForClass:[NSString class]];
OCMStub([suiteMock intValue]).andReturn(42);
caseMock = nil;
}

#pragma mark Tests suite mocks survive across test cases

- (void)testSuiteMockWorksHere
{
// Verify that a testSuite Mock made in +setUp doesn't get cleaned up until test suite is done.
// By verifying in two test cases we know this is true (See testSuiteMockWorksAndHere).
XCTAssertEqual([suiteMock intValue], 42);
}

- (void)testSuiteMockWorksAndHere
{
// Verify that a testSuite Mock made in +setUp doesn't get cleaned up until test suite is done.
// By verifying in two test cases we know this is true (See testSuiteMockWorksHere).
XCTAssertEqual([suiteMock intValue], 42);
}

#pragma mark Tests case mocks get stopped across test cases

- (void)setUpCaseMock
{
caseMock = [OCMockObject mockForClass:[NSString class]];
}

- (void)testCaseMockFailsEitherHere
{
// Set up a mock here that should get cleaned up (but the global pointer will still be non-nil)
// or test that the mock set up in testCaseMockFailsOrHere has had stop mocking called on it.
if (!caseMock)
{
[self setUpCaseMock];
}
else
{
XCTAssertThrows([caseMock stringValue],
@"Expected a throw here because the caseMock set up in "
@"testCaseMockFailsOrHere should have had stopMock called on it");
}
}

- (void)testCaseMockFailsOrHere
{
// Set up a mock here that should get cleaned up (but the global pointer will still be non-nil)
// or test that the mock set up in testCaseMockFailsOrHere has had stop mocking called on it.
if (!caseMock)
{
[self setUpCaseMock];
}
else
{
XCTAssertThrows([caseMock stringValue],
@"Expected a throw here because the caseMock set up in "
@"testCaseMockFailsEitherHere should have had stopMock called on it");
}
}


@end

0 comments on commit 56dd248

Please sign in to comment.