Skip to content

Commit 781ed9f

Browse files
committed
Fix for #444.
1 parent a439408 commit 781ed9f

File tree

6 files changed

+141
-37
lines changed

6 files changed

+141
-37
lines changed

Source/Changes.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Listing of notable changes by release. More detail is usually found in the Git
22
commit messages and/or the pull requests.
33

4+
OCMock 3.7.1 (unreleased)
5+
6+
* Fixed a bug that caused double-counting of method invocations on partial
7+
mocks under certain circumstances.
8+
9+
410
OCMock 3.7 (2020-07-15)
511

612
* Fixed mocking init methods when using ARC. This worked before but could

Source/OCMock/OCMockObject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
- (void)addStub:(OCMInvocationStub *)aStub;
6565
- (void)addExpectation:(OCMInvocationExpectation *)anExpectation;
66+
- (void)addInvocation:(NSInvocation *)anInvocation;
6667

6768
- (BOOL)handleInvocation:(NSInvocation *)anInvocation;
6869
- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation;

Source/OCMock/OCMockObject.m

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ - (void)addStub:(OCMInvocationStub *)aStub
134134
}
135135
}
136136

137+
- (OCMInvocationStub *)stubForInvocation:(NSInvocation *)anInvocation
138+
{
139+
@synchronized(stubs)
140+
{
141+
for(OCMInvocationStub *stub in stubs)
142+
if([stub matchesInvocation:anInvocation])
143+
return stub;
144+
return nil;
145+
}
146+
}
147+
137148
- (void)addExpectation:(OCMInvocationExpectation *)anExpectation
138149
{
139150
@synchronized(expectations)
@@ -150,6 +161,21 @@ - (void)assertInvocationsArrayIsPresent
150161
}
151162
}
152163

164+
- (void)addInvocation:(NSInvocation *)anInvocation
165+
{
166+
@synchronized(invocations)
167+
{
168+
// We can't do a normal retain arguments on anInvocation because its target/arguments/return
169+
// value could be self. That would produce a retain cycle self->invocations->anInvocation->self.
170+
// However we need to retain everything on anInvocation that isn't self because we expect them to
171+
// stick around after this method returns. Use our special method to retain just what's needed.
172+
// This still doesn't completely prevent retain cycles since any of the arguments could have a
173+
// strong reference to self. Those will have to be broken with manual calls to -stopMocking.
174+
[anInvocation retainObjectArgumentsExcludingObject:self];
175+
[invocations addObject:anInvocation];
176+
}
177+
}
178+
153179

154180
#pragma mark Public API
155181

@@ -374,37 +400,15 @@ - (void)forwardInvocation:(NSInvocation *)anInvocation
374400
- (BOOL)handleInvocation:(NSInvocation *)anInvocation
375401
{
376402
[self assertInvocationsArrayIsPresent];
377-
@synchronized(invocations)
378-
{
379-
// We can't do a normal retain arguments on anInvocation because its target/arguments/return
380-
// value could be self. That would produce a retain cycle self->invocations->anInvocation->self.
381-
// However we need to retain everything on anInvocation that isn't self because we expect them to
382-
// stick around after this method returns. Use our special method to retain just what's needed.
383-
// This still doesn't completely prevent retain cycles since any of the arguments could have a
384-
// strong reference to self. Those will have to be broken with manual calls to -stopMocking.
385-
[anInvocation retainObjectArgumentsExcludingObject:self];
386-
[invocations addObject:anInvocation];
387-
}
403+
[self addInvocation:anInvocation];
388404

389-
OCMInvocationStub *stub = nil;
390-
@synchronized(stubs)
391-
{
392-
for(stub in stubs)
393-
{
394-
// If the stub forwards its invocation to the real object, then we don't want to do
395-
// handleInvocation: yet, since forwarding the invocation to the real object could call a
396-
// method that is expected to happen after this one, which is bad if expectationOrderMatters
397-
// is YES
398-
if([stub matchesInvocation:anInvocation])
399-
break;
400-
}
401-
// Retain the stub in case it ends up being removed from stubs and expectations, since we still
402-
// have to call handleInvocation on the stub at the end
403-
[stub retain];
404-
}
405+
OCMInvocationStub *stub = [self stubForInvocation:anInvocation];
405406
if(stub == nil)
406407
return NO;
407408

409+
// Retain the stub in case it ends up being removed because we still need it at the end for handleInvocation:
410+
[stub retain];
411+
408412
BOOL removeStub = NO;
409413
@synchronized(expectations)
410414
{
@@ -418,8 +422,7 @@ - (BOOL)handleInvocation:(NSInvocation *)anInvocation
418422
}
419423

420424
// We can't check isSatisfied yet, since the stub won't be satisfied until we call
421-
// handleInvocation:, and we don't want to call handleInvocation: yes for the reason in the
422-
// comment above, since we'll still have the current expectation in the expectations array, which
425+
// handleInvocation: since we'll still have the current expectation in the expectations array, which
423426
// will cause an exception if expectationOrderMatters is YES and we're not ready for any future
424427
// expected methods to be called yet
425428
if(![(OCMInvocationExpectation *)stub isMatchAndReject])
@@ -437,9 +440,12 @@ - (BOOL)handleInvocation:(NSInvocation *)anInvocation
437440
}
438441
}
439442

440-
@try {
443+
@try
444+
{
441445
[stub handleInvocation:anInvocation];
442-
} @finally {
446+
}
447+
@finally
448+
{
443449
[stub release];
444450
}
445451

Source/OCMock/OCPartialMockObject.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818

1919
@interface OCPartialMockObject : OCClassMockObject
2020
{
21-
NSObject *realObject;
21+
NSObject *realObject;
22+
NSInvocation *invocationFromMock;
2223
}
2324

2425
- (id)initWithObject:(NSObject *)anObject;

Source/OCMock/OCPartialMockObject.m

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ - (void)addStub:(OCMInvocationStub *)aStub
105105
[self setupForwarderForSelector:[[aStub recordedInvocation] selector]];
106106
}
107107

108+
- (void)addInvocation:(NSInvocation *)anInvocation
109+
{
110+
// If the mock invokes a method on the real object we end up here a second time, but because
111+
// the mock has added the invocation already we do not want to add it again.
112+
if((invocationFromMock == nil) || ([anInvocation selector] != [invocationFromMock selector]))
113+
[super addInvocation:anInvocation];
114+
}
115+
108116
- (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
109117
{
110118
// In the case of an init that is called on a mock we must return the mock instance and
@@ -118,12 +126,16 @@ - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation
118126
targetReceivingInit = [anInvocation target];
119127
[realObject retain];
120128
}
129+
130+
invocationFromMock = anInvocation;
121131
[anInvocation invokeWithTarget:realObject];
122-
if (targetReceivingInit)
132+
invocationFromMock = nil;
133+
134+
if(targetReceivingInit)
123135
{
124136
id returnVal;
125137
[anInvocation getReturnValue:&returnVal];
126-
if (returnVal == realObject)
138+
if(returnVal == realObject)
127139
{
128140
[anInvocation setReturnValue:&self];
129141
[realObject release];
@@ -247,7 +259,6 @@ - (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation
247259
}
248260

249261

250-
251262
#pragma mark Verification handling
252263

253264
- (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count

Source/OCMockTests/OCMockObjectPartialMocksTests.m

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#import <CoreData/CoreData.h>
1818
#import <XCTest/XCTest.h>
1919
#import <OCMock/OCMock.h>
20+
#import "OCPartialMockObject.h"
2021
#import "TestClassWithCustomReferenceCounting.h"
2122

2223
#if TARGET_OS_IPHONE
@@ -170,8 +171,6 @@ - (NSString *)categoryMethod
170171
@end
171172

172173

173-
174-
175174
@interface OCMockObjectPartialMocksTests : XCTestCase
176175
{
177176
int numKVOCallbacks;
@@ -209,6 +208,28 @@ @implementation OCTestManagedObject
209208
@end
210209

211210

211+
#pragma mark Category for testing
212+
213+
@interface OCPartialMockObject(AccessToInvocationsForTesting)
214+
215+
- (NSArray *)invocationsExcludingInitialize;
216+
217+
@end
218+
219+
@implementation OCPartialMockObject(AccessToInvocationsForTesting)
220+
221+
- (NSArray *)invocationsExcludingInitialize
222+
{
223+
NSMutableArray *filteredInvocations = [[NSMutableArray alloc] init];
224+
for(NSInvocation *i in invocations)
225+
if([NSStringFromSelector([i selector]) hasSuffix:@"initialize"] == NO)
226+
[filteredInvocations addObject:i];
227+
228+
return filteredInvocations;
229+
}
230+
231+
@end
232+
212233

213234
@implementation OCMockObjectPartialMocksTests
214235

@@ -280,6 +301,64 @@ - (void)testInvocationsOfNSObjectCategoryMethodsCanBeStubbed
280301
}
281302

282303

304+
#pragma mark Tests for remembering invocations for later verification
305+
306+
- (void)testRecordsInvocationWhenRealObjectIsUsed
307+
{
308+
TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init];
309+
id mock = [OCMockObject partialMockForObject:realObject];
310+
311+
[realObject foo];
312+
313+
XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]);
314+
}
315+
316+
- (void)testRecordsInvocationWhenMockIsUsed
317+
{
318+
TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init];
319+
id mock = [OCMockObject partialMockForObject:realObject];
320+
321+
[mock foo];
322+
323+
XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]);
324+
}
325+
326+
- (void)testRecordsInvocationWhenRealObjectIsUsedAndMethodIsStubbed
327+
{
328+
TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init];
329+
id mock = [OCMockObject partialMockForObject:realObject];
330+
[[[mock stub] andReturn:@"bar"] foo];
331+
332+
id res = [realObject foo];
333+
334+
XCTAssertEqualObjects(@"bar", res);
335+
XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]);
336+
}
337+
338+
- (void)testRecordsInvocationWhenMockIsUsedAndMethodIsStubbed
339+
{
340+
TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init];
341+
id mock = [OCMockObject partialMockForObject:realObject];
342+
[[[mock stub] andReturn:@"bar"] foo];
343+
344+
id res = [mock foo];
345+
346+
XCTAssertEqualObjects(@"bar", res);
347+
XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]);
348+
}
349+
350+
- (void)testRecordsInvocationWhenMockIsUsedAndMethodIsStubbedAndForwardsToRealObject
351+
{
352+
TestClassWithSimpleMethod *realObject = [[TestClassWithSimpleMethod alloc] init];
353+
id mock = [OCMockObject partialMockForObject:realObject];
354+
[[[mock stub] andForwardToRealObject] foo];
355+
356+
[mock foo];
357+
358+
XCTAssertEqual(1, [[mock invocationsExcludingInitialize] count]);
359+
}
360+
361+
283362
#pragma mark Tests for behaviour when setting up partial mocks
284363

285364
- (void)testPartialMockClassOverrideReportsOriginalClass

0 commit comments

Comments
 (0)