From 09fdbffc6195268bb2c02cf47035625fcf24229c Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 13 Jun 2017 17:43:43 -0700 Subject: [PATCH] Fix memory leaks, add section-object support to new test harness --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 +- Source/ASRunLoopQueue.h | 2 + Source/ASRunLoopQueue.mm | 23 ++++ Source/Details/ASWeakSet.m | 2 +- Tests/ASCollectionModernDataSourceTests.m | 153 ++++++++++++++++------ Tests/ASTestCase.h | 6 + Tests/ASTestCase.m | 62 ++++++++- Tests/OCMockObject+ASAdditions.h | 4 + Tests/OCMockObject+ASAdditions.m | 27 +++- 9 files changed, 236 insertions(+), 47 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index bab808a16..2cd660f06 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -370,8 +370,8 @@ CCA282CD1E9EB73E0037E8B7 /* ASTipNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */; }; CCA282D01E9EBF6C0037E8B7 /* ASTipsWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */; }; CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */; }; - CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62D1EECC2A80060C137 /* ASAssert.m */; }; CCA5F62C1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */; }; + CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62D1EECC2A80060C137 /* ASAssert.m */; }; CCB2F34D1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */; }; CCB338E41EEE11160081F21A /* OCMockObject+ASAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB338E31EEE11160081F21A /* OCMockObject+ASAdditions.m */; }; CCB338E71EEE27760081F21A /* ASTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = CCB338E61EEE27760081F21A /* ASTestCase.m */; }; @@ -832,9 +832,9 @@ CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTipNode.m; sourceTree = ""; }; CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipsWindow.h; sourceTree = ""; }; CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTipsWindow.m; sourceTree = ""; }; - CCA5F62D1EECC2A80060C137 /* ASAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASAssert.m; sourceTree = ""; }; CCA5F62A1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+ASTestHelpers.h"; sourceTree = ""; }; CCA5F62B1EEC9E9B0060C137 /* NSInvocation+ASTestHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+ASTestHelpers.m"; sourceTree = ""; }; + CCA5F62D1EECC2A80060C137 /* ASAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASAssert.m; sourceTree = ""; }; CCB2F34C1D63CCC6004E6DE9 /* ASDisplayNodeSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeSnapshotTests.m; sourceTree = ""; }; CCB338E21EEE11160081F21A /* OCMockObject+ASAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OCMockObject+ASAdditions.h"; sourceTree = ""; }; CCB338E31EEE11160081F21A /* OCMockObject+ASAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OCMockObject+ASAdditions.m"; sourceTree = ""; }; diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index 486b81628..7ff563f40 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -51,6 +51,8 @@ AS_SUBCLASSING_RESTRICTED + (instancetype)sharedDeallocationQueue; +- (void)test_drain; + - (void)releaseObjectInBackground:(id)object; @end diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 444236aeb..7597e47d2 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -143,6 +143,29 @@ - (void)stop _thread = nil; } +- (void)test_drain +{ + [self performSelector:@selector(_test_drain) onThread:_thread withObject:nil waitUntilDone:YES]; +} + +- (void)_test_drain +{ + while (true) { + @autoreleasepool { + _queueLock.lock(); + std::deque currentQueue = _queue; + _queue = std::deque(); + _queueLock.unlock(); + + if (currentQueue.empty()) { + return; + } else { + currentQueue.clear(); + } + } + } +} + - (void)_stop { CFRunLoopStop(CFRunLoopGetCurrent()); diff --git a/Source/Details/ASWeakSet.m b/Source/Details/ASWeakSet.m index 0e2a3263c..a469d56d3 100644 --- a/Source/Details/ASWeakSet.m +++ b/Source/Details/ASWeakSet.m @@ -27,7 +27,7 @@ - (instancetype)init { self = [super init]; if (self) { - _hashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality]; + _hashTable = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory | NSHashTableObjectPointerPersonality]; } return self; } diff --git a/Tests/ASCollectionModernDataSourceTests.m b/Tests/ASCollectionModernDataSourceTests.m index 88c1123d0..4a53bda1a 100644 --- a/Tests/ASCollectionModernDataSourceTests.m +++ b/Tests/ASCollectionModernDataSourceTests.m @@ -23,21 +23,28 @@ @interface ASCollectionModernDataSourceTests : ASTestCase @interface ASTestCellNode : ASCellNode @end +@interface ASTestSection : NSObject +@property (nonatomic, readonly) NSMutableArray *viewModels; +@end + @implementation ASCollectionModernDataSourceTests { @private id mockDataSource; UIWindow *window; UIViewController *viewController; ASCollectionNode *collectionNode; - NSMutableArray *sections; + NSMutableArray *sections; } - (void)setUp { [super setUp]; // Default is 2 sections: 2 items in first, 1 item in second. sections = [NSMutableArray array]; - [sections addObject:[NSMutableArray arrayWithObjects:[NSObject new], [NSObject new], nil]]; - [sections addObject:[NSMutableArray arrayWithObjects:[NSObject new], nil]]; + [sections addObject:[ASTestSection new]]; + [sections[0].viewModels addObject:[NSObject new]]; + [sections[0].viewModels addObject:[NSObject new]]; + [sections addObject:[ASTestSection new]]; + [sections[1].viewModels addObject:[NSObject new]]; window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; viewController = [[UIViewController alloc] init]; @@ -54,6 +61,7 @@ - (void)setUp { @selector(collectionNode:numberOfItemsInSection:), @selector(collectionNode:nodeBlockForItemAtIndexPath:), @selector(collectionNode:viewModelForItemAtIndexPath:), + @selector(collectionNode:contextForSection:), nil]; [mockDataSource setExpectationOrderMatters:YES]; @@ -64,7 +72,6 @@ - (void)setUp { - (void)tearDown { [collectionNode waitUntilAllUpdatesAreCommitted]; - OCMVerifyAll(mockDataSource); [super tearDown]; } @@ -82,11 +89,12 @@ - (void)testReloadingAnItem // Reload at (0, 0) NSIndexPath *reloadedPath = [NSIndexPath indexPathForItem:0 inSection:0]; - [self performUpdateReloadingItems:@{ reloadedPath: [NSObject new] } - reloadMappings:@{ reloadedPath: reloadedPath } - insertingItems:nil - deletingItems:nil - skippedReloadIndexPaths:nil]; + [self performUpdateReloadingSections:nil + reloadingItems:@{ reloadedPath: [NSObject new] } + reloadMappings:@{ reloadedPath: reloadedPath } + insertingItems:nil + deletingItems:nil + skippedReloadIndexPaths:nil]; } - (void)testInsertingAnItem @@ -96,11 +104,12 @@ - (void)testInsertingAnItem // Insert at (1, 0) NSIndexPath *insertedPath = [NSIndexPath indexPathForItem:0 inSection:1]; - [self performUpdateReloadingItems:nil - reloadMappings:nil - insertingItems:@{ insertedPath: [NSObject new] } - deletingItems:nil - skippedReloadIndexPaths:nil]; + [self performUpdateReloadingSections:nil + reloadingItems:nil + reloadMappings:nil + insertingItems:@{ insertedPath: [NSObject new] } + deletingItems:nil + skippedReloadIndexPaths:nil]; } - (void)testReloadingAnItemWithACompatibleViewModel @@ -115,17 +124,27 @@ - (void)testReloadingAnItemWithACompatibleViewModel // Cell node should get -canUpdateToViewModel: id mockCellNode = [collectionNode nodeForItemAtIndexPath:reloadedPath]; - [mockCellNode setExpectationOrderMatters:YES]; OCMExpect([mockCellNode canUpdateToViewModel:viewModel]) .andReturn(YES); - [self performUpdateReloadingItems:@{ reloadedPath: viewModel } - reloadMappings:@{ reloadedPath: [NSIndexPath indexPathForItem:0 inSection:0] } - insertingItems:nil - deletingItems:@[ deletedPath ] - skippedReloadIndexPaths:@[ reloadedPath ]]; - - OCMVerifyAll(mockCellNode); + [self performUpdateReloadingSections:nil + reloadingItems:@{ reloadedPath: viewModel } + reloadMappings:@{ reloadedPath: [NSIndexPath indexPathForItem:0 inSection:0] } + insertingItems:nil + deletingItems:@[ deletedPath ] + skippedReloadIndexPaths:@[ reloadedPath ]]; +} + +- (void)testReloadingASection +{ + [self loadInitialData]; + + [self performUpdateReloadingSections:@{ @0: [ASTestSection new] } + reloadingItems:nil + reloadMappings:nil + insertingItems:nil + deletingItems:nil + skippedReloadIndexPaths:nil]; } #pragma mark - Helpers @@ -137,14 +156,19 @@ - (void)loadInitialData // It reads all the counts [self expectDataSourceCountMethods]; + // It reads each section object. + for (NSInteger section = 0; section < sections.count; section++) { + [self expectContextMethodForSection:section]; + } + // It reads the contents for each item. for (NSInteger section = 0; section < sections.count; section++) { - NSArray *items = sections[section]; + NSArray *viewModels = sections[section].viewModels; // For each item: - for (NSInteger i = 0; i < items.count; i++) { + for (NSInteger i = 0; i < viewModels.count; i++) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section]; - [self expectViewModelMethodForItemAtIndexPath:indexPath viewModel:items[i]]; + [self expectViewModelMethodForItemAtIndexPath:indexPath viewModel:viewModels[i]]; [self expectNodeBlockMethodForItemAtIndexPath:indexPath]; } } @@ -172,9 +196,8 @@ - (void)expectDataSourceCountMethods // For each section: // Note: Skip fast enumeration for readability. for (NSInteger section = 0; section < sections.count; section++) { - NSInteger itemCount = sections[section].count; OCMExpect([mockDataSource collectionNode:collectionNode numberOfItemsInSection:section]) - .andReturn(itemCount); + .andReturn(sections[section].viewModels.count); } } @@ -184,15 +207,24 @@ - (void)expectViewModelMethodForItemAtIndexPath:(NSIndexPath *)indexPath viewMod .andReturn(viewModel); } +- (void)expectContextMethodForSection:(NSInteger)section +{ + OCMExpect([mockDataSource collectionNode:collectionNode contextForSection:section]) + .andReturn(sections[section]); +} + - (void)expectNodeBlockMethodForItemAtIndexPath:(NSIndexPath *)indexPath { OCMExpect([mockDataSource collectionNode:collectionNode nodeBlockForItemAtIndexPath:indexPath]) .andReturn((ASCellNodeBlock)^{ ASCellNode *node = [ASTestCellNode new]; // Generating multiple partial mocks of the same class is not thread-safe. + id mockNode; @synchronized (NSNull.null) { - return OCMPartialMock(node); + mockNode = OCMPartialMock(node); } + [mockNode setExpectationOrderMatters:YES]; + return mockNode; }); } @@ -203,14 +235,19 @@ - (void)assertCollectionNodeContent XCTAssertEqual(collectionNode.numberOfSections, sections.count); for (NSInteger section = 0; section < sections.count; section++) { - NSArray *items = sections[section]; + ASTestSection *sectionObject = sections[section]; + NSArray *viewModels = sectionObject.viewModels; + + // Assert section object + XCTAssertEqualObjects([collectionNode contextForSection:section], sectionObject); + // Assert item count - XCTAssertEqual([collectionNode numberOfItemsInSection:section], items.count); - for (NSInteger item = 0; item < items.count; item++) { + XCTAssertEqual([collectionNode numberOfItemsInSection:section], viewModels.count); + for (NSInteger item = 0; item < viewModels.count; item++) { // Assert view model // Could use pointer equality but the error message is less readable. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; - id viewModel = sections[indexPath.section][indexPath.item]; + id viewModel = viewModels[indexPath.item]; XCTAssertEqualObjects(viewModel, [collectionNode viewModelForItemAtIndexPath:indexPath]); ASCellNode *node = [collectionNode nodeForItemAtIndexPath:indexPath]; XCTAssertEqualObjects(node.viewModel, viewModel); @@ -224,30 +261,39 @@ - (void)assertCollectionNodeContent * * skippedReloadIndexPaths are the old index paths for nodes that should use -canUpdateToViewModel: instead of being refetched. */ -- (void)performUpdateReloadingItems:(NSDictionary *)reloadedItems - reloadMappings:(NSDictionary *)reloadMappings - insertingItems:(NSDictionary *)insertedItems - deletingItems:(NSArray *)deletedItems - skippedReloadIndexPaths:(NSArray *)skippedReloadIndexPaths +- (void)performUpdateReloadingSections:(NSDictionary *)reloadedSections + reloadingItems:(NSDictionary *)reloadedItems + reloadMappings:(NSDictionary *)reloadMappings + insertingItems:(NSDictionary *)insertedItems + deletingItems:(NSArray *)deletedItems + skippedReloadIndexPaths:(NSArray *)skippedReloadIndexPaths { [collectionNode performBatchUpdates:^{ // First update our data source. [reloadedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { - sections[key.section][key.item] = obj; + sections[key.section].viewModels[key.item] = obj; + }]; + [reloadedSections enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + sections[key.integerValue] = obj; }]; // Deletion paths, sorted descending for (NSIndexPath *indexPath in [deletedItems sortedArrayUsingSelector:@selector(compare:)].reverseObjectEnumerator) { - [sections[indexPath.section] removeObjectAtIndex:indexPath.item]; + [sections[indexPath.section].viewModels removeObjectAtIndex:indexPath.item]; } // Insertion paths, sorted ascending. NSArray *insertionsSortedAcending = [insertedItems.allKeys sortedArrayUsingSelector:@selector(compare:)]; for (NSIndexPath *indexPath in insertionsSortedAcending) { - [sections[indexPath.section] insertObject:insertedItems[indexPath] atIndex:indexPath.item]; + [sections[indexPath.section].viewModels insertObject:insertedItems[indexPath] atIndex:indexPath.item]; } // Then update the collection node. + NSMutableIndexSet *reloadedSectionIndexes = [NSMutableIndexSet indexSet]; + for (NSNumber *i in reloadedSections) { + [reloadedSectionIndexes addIndex:i.integerValue]; + } + [collectionNode reloadSections:reloadedSectionIndexes]; [collectionNode reloadItemsAtIndexPaths:reloadedItems.allKeys]; [collectionNode deleteItemsAtIndexPaths:deletedItems]; [collectionNode insertItemsAtIndexPaths:insertedItems.allKeys]; @@ -260,6 +306,17 @@ - (void)performUpdateReloadingItems:(NSDictionary *)reloadedI // Combine reloads + inserts and expect them to load content for all of them, in ascending order. NSMutableDictionary *insertsPlusReloads = [NSMutableDictionary dictionary]; [insertsPlusReloads addEntriesFromDictionary:insertedItems]; + + // Go through reloaded sections and add all their items into `insertsPlusReloads` + [reloadedSectionIndexes enumerateIndexesUsingBlock:^(NSUInteger section, BOOL * _Nonnull stop) { + [self expectContextMethodForSection:section]; + NSArray *viewModels = sections[section].viewModels; + for (NSInteger i = 0; i < viewModels.count; i++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:section]; + insertsPlusReloads[indexPath] = viewModels[i]; + } + }]; + [reloadedItems enumerateKeysAndObjectsUsingBlock:^(NSIndexPath * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { insertsPlusReloads[reloadMappings[key]] = obj; }]; @@ -280,6 +337,8 @@ - (void)performUpdateReloadingItems:(NSDictionary *)reloadedI @end +#pragma mark - Other Objects + @implementation ASTestCellNode - (BOOL)canUpdateToViewModel:(id)viewModel @@ -289,3 +348,17 @@ - (BOOL)canUpdateToViewModel:(id)viewModel } @end + +@implementation ASTestSection +@synthesize collectionView; +@synthesize sectionName; + +- (instancetype)init +{ + if (self = [super init]) { + _viewModels = [NSMutableArray array]; + } + return self; +} + +@end diff --git a/Tests/ASTestCase.h b/Tests/ASTestCase.h index c023abb8f..4868f64d0 100644 --- a/Tests/ASTestCase.h +++ b/Tests/ASTestCase.h @@ -12,6 +12,12 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface ASTestCase : XCTestCase +@property (class, nonatomic, nullable, readonly) ASTestCase *currentTestCase; + @end + +NS_ASSUME_NONNULL_END diff --git a/Tests/ASTestCase.m b/Tests/ASTestCase.m index 854179d3c..5b4f9dddf 100644 --- a/Tests/ASTestCase.m +++ b/Tests/ASTestCase.m @@ -12,8 +12,22 @@ #import "ASTestCase.h" #import +#import +#import +#import "OCMockObject+ASAdditions.h" -@implementation ASTestCase +static __weak ASTestCase *currentTestCase; + +@implementation ASTestCase { + ASWeakSet *registeredMockObjects; +} + +- (void)setUp +{ + [super setUp]; + currentTestCase = self; + registeredMockObjects = [ASWeakSet new]; +} - (void)tearDown { @@ -22,12 +36,15 @@ - (void)tearDown for (UIWindow *window in [UIApplication sharedApplication].windows) { [window resignKeyWindow]; window.hidden = YES; + window.rootViewController = nil; for (UIView *view in window.subviews) { [view removeFromSuperview]; } } // Set nil for all our subclasses' ivars. Use setValue:forKey: so memory is managed correctly. + // This is important to do _inside_ the test-perform, so that we catch any issues caused by the + // deallocation, and so that we're inside the @autoreleasepool for the test invocation. Class c = [self class]; while (c != [ASTestCase class]) { unsigned int ivarCount; @@ -40,11 +57,52 @@ - (void)tearDown if (ivars) { free(ivars); } - + c = [c superclass]; } + + for (OCMockObject *mockObject in registeredMockObjects) { + OCMVerifyAll(mockObject); + [mockObject stopMocking]; + + // Invocations retain arguments, which may cause retain cycles. + // Manually clear them all out. + NSMutableArray *invocations = object_getIvar(mockObject, class_getInstanceVariable(OCMockObject.class, "invocations")); + [invocations removeAllObjects]; + } + + // Go ahead and spin the run loop before finishing, so the system + // unregisters/cleans up whatever possible. + [NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture]; [super tearDown]; } +- (void)invokeTest +{ + // This will call setup, run, then teardown. + @autoreleasepool { + [super invokeTest]; + } + + // Now that the autorelease pool is drained, drain the dealloc queue also. + [[ASDeallocQueue sharedDeallocationQueue] test_drain]; +} + ++ (ASTestCase *)currentTestCase +{ + return currentTestCase; +} + +@end + +@implementation ASTestCase (OCMockObjectRegistering) + +- (void)registerMockObject:(id)mockObject +{ + @synchronized (registeredMockObjects) { + [registeredMockObjects addObject:mockObject]; + } +} + @end diff --git a/Tests/OCMockObject+ASAdditions.h b/Tests/OCMockObject+ASAdditions.h index b2966922a..a8e700491 100644 --- a/Tests/OCMockObject+ASAdditions.h +++ b/Tests/OCMockObject+ASAdditions.h @@ -14,6 +14,10 @@ @interface OCMockObject (ASAdditions) +/** + * NOTE: All OCMockObjects created during an ASTestCase call OCMVerifyAll during -tearDown. + */ + /** * A method to manually specify which optional protocol methods should return YES * from -respondsToSelector:. diff --git a/Tests/OCMockObject+ASAdditions.m b/Tests/OCMockObject+ASAdditions.m index 7e7529f51..8b3235fb2 100644 --- a/Tests/OCMockObject+ASAdditions.m +++ b/Tests/OCMockObject+ASAdditions.m @@ -10,18 +10,32 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import "OCMockObject+ASAdditions.h" + +#import #import "ASInternalHelpers.h" #import +#import "ASTestCase.h" + +@interface ASTestCase (OCMockObjectRegistering) + +- (void)registerMockObject:(id)mockObject; + +@end @implementation OCMockObject (ASAdditions) + (void)load { - // Swap [OCProtocolMockObject respondsToSelector:] with [(self) swizzled_protocolMockRespondsToSelector:] + // [OCProtocolMockObject respondsToSelector:] <-> [(self) swizzled_protocolMockRespondsToSelector:] Method orig = class_getInstanceMethod(OCMockObject.protocolMockObjectClass, @selector(respondsToSelector:)); Method new = class_getInstanceMethod(self, @selector(swizzled_protocolMockRespondsToSelector:)); method_exchangeImplementations(orig, new); + + // init <-> swizzled_init + Method origInit = class_getInstanceMethod([OCMockObject class], @selector(init)); + Method newInit = class_getInstanceMethod(self, @selector(swizzled_init)); + method_exchangeImplementations(origInit, newInit); } /// Since OCProtocolMockObject is private, use this method to get the class. @@ -120,4 +134,13 @@ - (BOOL)swizzled_protocolMockRespondsToSelector:(SEL)aSelector return [self implementsOptionalProtocolMethod:aSelector]; } +// Whenever a mock object is initted, register it with the current test case +// so that it gets verified and its invocations are cleared during -tearDown. +- (instancetype)swizzled_init +{ + [self swizzled_init]; + [ASTestCase.currentTestCase registerMockObject:self]; + return self; +} + @end