|
31 | 31 | #import "OCMExpectationRecorder.h"
|
32 | 32 | #import "OCMQuantifier.h"
|
33 | 33 |
|
| 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; |
34 | 45 |
|
35 | 46 | @implementation OCMockObject
|
36 | 47 |
|
37 | 48 | #pragma mark Class initialisation
|
38 | 49 |
|
39 | 50 | + (void)initialize
|
40 | 51 | {
|
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 | + } |
43 | 56 | }
|
44 | 57 |
|
| 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 | +} |
45 | 87 |
|
46 | 88 | #pragma mark Factory methods
|
47 | 89 |
|
@@ -109,6 +151,7 @@ - (instancetype)init
|
109 | 151 | expectations = [[NSMutableArray alloc] init];
|
110 | 152 | exceptions = [[NSMutableArray alloc] init];
|
111 | 153 | invocations = [[NSMutableArray alloc] init];
|
| 154 | + [OCMockObject recordAMockToStop:self]; |
112 | 155 | return self;
|
113 | 156 | }
|
114 | 157 |
|
@@ -144,11 +187,22 @@ - (void)addExpectation:(OCMInvocationExpectation *)anExpectation
|
144 | 187 |
|
145 | 188 | - (void)assertInvocationsArrayIsPresent
|
146 | 189 | {
|
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]; |
149 | 193 | }
|
150 | 194 | }
|
151 | 195 |
|
| 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 | +} |
152 | 206 |
|
153 | 207 | #pragma mark Public API
|
154 | 208 |
|
@@ -448,7 +502,7 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
|
448 | 502 | {
|
449 | 503 | if(isNice == NO)
|
450 | 504 | {
|
451 |
| - [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@", |
| 505 | + [OCMockObject logMatcherIssue:@"%@: unexpected method invoked: %@ %@", |
452 | 506 | [self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]];
|
453 | 507 | }
|
454 | 508 | }
|
@@ -508,4 +562,83 @@ - (NSString *)_stubDescriptions:(BOOL)onlyExpectations
|
508 | 562 | }
|
509 | 563 |
|
510 | 564 |
|
| 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 | + |
511 | 644 | @end
|
0 commit comments