Skip to content

Commit f49ac76

Browse files
committed
Fixed bugs regarding class method mocking in class hierarchies.
1 parent bc53dbc commit f49ac76

File tree

3 files changed

+121
-28
lines changed

3 files changed

+121
-28
lines changed

Source/Changes.txt

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Chronological listing of changes. More detail is usually found in the Git commit messages
22
and/or the pull requests.
33

4+
2013-07-15
5+
6+
* Fixed several bugs regarding class method mocking in class hierarchies.
7+
8+
49
2013-07-03
510

611
* Fixed bug preventing the same class method to be expected more than once.
@@ -14,7 +19,7 @@ and/or the pull requests.
1419

1520
2013-06-19
1621

17-
* Constraints implement NSCopying for Xcode 5 compatibility.
22+
* Constraints implement NSCopying for OS X 10.9 SDK compatibility.
1823

1924

2025
2013-03-14

Source/OCMock/OCClassMockObject.m

+35-26
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ + (OCClassMockObject *)existingMockForClass:(Class)aClass
3939
{
4040
@synchronized(mockTable)
4141
{
42-
OCClassMockObject *mock = [[mockTable objectForKey:[NSValue valueWithNonretainedObject:aClass]] nonretainedObjectValue];
42+
OCClassMockObject *mock = nil;
43+
while((mock == nil) && (aClass != nil))
44+
{
45+
mock = [[mockTable objectForKey:[NSValue valueWithNonretainedObject:aClass]] nonretainedObjectValue];
46+
aClass = class_getSuperclass(aClass);
47+
}
4348
if(mock == nil)
44-
[NSException raise:NSInternalInconsistencyException format:@"No mock for class %p", aClass];
49+
[NSException raise:NSInternalInconsistencyException format:@"No mock for class %@", NSStringFromClass(aClass)];
4550
return mock;
4651
}
4752
}
@@ -86,42 +91,46 @@ - (void)setupClassForClassMethodMocking
8691

8792
replacedClassMethods = [[NSMutableDictionary alloc] init];
8893
[[self class] rememberMock:self forClass:mockedClass];
89-
90-
Method myForwardInvocationMethod = class_getInstanceMethod([self class], @selector(forwardInvocationForClassObject:));
91-
IMP myForwardInvocationImp = method_getImplementation(myForwardInvocationMethod);
92-
const char *forwardInvocationTypes = method_getTypeEncoding(myForwardInvocationMethod);
93-
Class metaClass = objc_getMetaClass(class_getName(mockedClass));
94-
95-
IMP replacedForwardInvocationImp = class_replaceMethod(metaClass, @selector(forwardInvocation:), myForwardInvocationImp, forwardInvocationTypes);
96-
97-
[replacedClassMethods setObject:[NSValue valueWithPointer:replacedForwardInvocationImp] forKey:NSStringFromSelector(@selector(forwardInvocation:))];
94+
95+
Method method = class_getClassMethod(mockedClass, @selector(forwardInvocation:));
96+
IMP originalIMP = method_getImplementation(method);
97+
[replacedClassMethods setObject:[NSValue valueWithPointer:originalIMP] forKey:NSStringFromSelector(@selector(forwardInvocation:))];
98+
99+
Method myForwardMethod = class_getInstanceMethod([self class], @selector(forwardInvocationForClassObject:));
100+
IMP myForwardIMP = method_getImplementation(myForwardMethod);
101+
Class metaClass = objc_getMetaClass(class_getName(mockedClass));
102+
class_replaceMethod(metaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod));
98103
}
99104

100105
- (void)setupForwarderForClassMethodSelector:(SEL)selector
101106
{
102107
if([replacedClassMethods objectForKey:NSStringFromSelector(selector)] != nil)
103108
return;
104109

105-
Method originalMethod = class_getClassMethod(mockedClass, selector);
106-
Class metaClass = objc_getMetaClass(class_getName(mockedClass));
107-
108-
IMP forwarderImp = [metaClass instanceMethodForSelector:@selector(aMethodThatMustNotExist)];
109-
IMP replacedMethod = class_replaceMethod(metaClass, method_getName(originalMethod), forwarderImp, method_getTypeEncoding(originalMethod));
110+
// We're using class_replaceMethod and not method_setImplementation to make sure
111+
// the stub is definitely added to the mocked class, and not a superclass. However,
112+
// we still get the originalIMP from the method in case it was actually implemented
113+
// in a superclass.
114+
Method method = class_getClassMethod(mockedClass, selector);
115+
IMP originalIMP = method_getImplementation(method);
116+
[replacedClassMethods setObject:[NSValue valueWithPointer:originalIMP] forKey:NSStringFromSelector(selector)];
117+
118+
Class metaClass = objc_getMetaClass(class_getName(mockedClass));
119+
IMP forwarderIMP = [metaClass instanceMethodForSelector:@selector(aMethodThatMustNotExist)];
120+
class_replaceMethod(metaClass, method_getName(method), forwarderIMP, method_getTypeEncoding(method));
110121

111-
[replacedClassMethods setObject:[NSValue valueWithPointer:replacedMethod] forKey:NSStringFromSelector(selector)];
112122
}
113123

114124
- (void)removeForwarderForClassMethodSelector:(SEL)selector
115125
{
116-
Class metaClass = objc_getMetaClass(class_getName(mockedClass));
117-
NSValue *originalMethodPointer = [replacedClassMethods objectForKey:NSStringFromSelector(selector)];
118-
IMP originalMethod = [originalMethodPointer pointerValue];
119-
if(originalMethod) {
120-
class_replaceMethod(metaClass, selector, originalMethod, 0);
121-
} else {
122-
IMP forwarderImp = [metaClass instanceMethodForSelector:@selector(aMethodThatMustNotExist)];
123-
class_replaceMethod(metaClass, selector, forwarderImp, 0);
124-
}
126+
IMP originalIMP = [[replacedClassMethods objectForKey:NSStringFromSelector(selector)] pointerValue];
127+
if(originalIMP == NULL)
128+
{
129+
[NSException raise:NSInternalInconsistencyException format:@"%@: Trying to remove stub for class method %@, but no previous implementation available.",
130+
[self description], NSStringFromSelector(selector)];
131+
}
132+
Method method = class_getClassMethod(mockedClass, selector);
133+
method_setImplementation(method, originalIMP);
125134
}
126135

127136
- (void)forwardInvocationForClassObject:(NSInvocation *)anInvocation

Source/OCMockTests/OCMockObjectClassMethodMockingTests.m

+80-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ - (NSString *)bar
3535
@end
3636

3737

38+
@interface TestSubclassWithClassMethods : TestClassWithClassMethods
39+
40+
@end
41+
42+
@implementation TestSubclassWithClassMethods
43+
44+
@end
45+
46+
3847

3948
@implementation OCMockObjectClassMethodMockingTests
4049

@@ -59,7 +68,6 @@ - (void)testCanExpectTheSameClassMethodMoreThanOnce
5968
STAssertEqualObjects(@"mocked-foo2", [TestClassWithClassMethods foo], @"Should have stubbed class method 'foo2'.");
6069
}
6170

62-
6371
- (void)testClassReceivesMethodsAfterStopWasCalled
6472
{
6573
id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]];
@@ -80,6 +88,77 @@ - (void)testClassReceivesMethodAgainWhenExpectedCallOccurred
8088
STAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should have 'unstubbed' method.");
8189
}
8290

91+
- (void)testCanStubClassMethodFromMockForSubclass
92+
{
93+
id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]];
94+
95+
[[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo];
96+
STAssertEqualObjects(@"mocked-subclass", [TestSubclassWithClassMethods foo], @"Should have stubbed method.");
97+
STAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should not have stubbed method in superclass.");
98+
}
99+
100+
- (void)testSuperclassReceivesMethodsAfterStopWasCalled
101+
{
102+
id mock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]];
103+
104+
[[[[mock stub] classMethod] andReturn:@"mocked"] foo];
105+
[mock stopMocking];
106+
107+
STAssertEqualObjects(@"Foo-ClassMethod", [TestSubclassWithClassMethods foo], @"Should not have stubbed class method.");
108+
}
109+
110+
- (void)testCanReplaceSameMethodInSubclassAfterSuperclassMockWasStopped
111+
{
112+
id superclassMock = [OCMockObject mockForClass:[TestClassWithClassMethods class]];
113+
id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]];
114+
115+
[[[[superclassMock stub] classMethod] andReturn:@"mocked-superclass"] foo];
116+
[superclassMock stopMocking];
117+
118+
[[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo];
119+
STAssertEqualObjects(@"mocked-subclass", [TestSubclassWithClassMethods foo], @"Should have stubbed method");
120+
}
121+
122+
- (void)testCanReplaceSameMethodInSuperclassAfterSubclassMockWasStopped
123+
{
124+
id superclassMock = [OCMockObject mockForClass:[TestClassWithClassMethods class]];
125+
id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]];
126+
127+
[[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo];
128+
[subclassMock stopMocking];
129+
130+
[[[[superclassMock stub] classMethod] andReturn:@"mocked-superclass"] foo];
131+
STAssertEqualObjects(@"mocked-superclass", [TestClassWithClassMethods foo], @"Should have stubbed method");
132+
}
133+
134+
// The following test does not verify behaviour; it shows a problem. It only passes when run in
135+
// isolation because otherwise the other tests cause the problem that this test demonstrates.
136+
137+
- (void)_ignore_testShowThatStubbingSuperclassMethodInSubclassLeavesImplementationInSubclass
138+
{
139+
// stage 1: stub in superclass affects both superclass and subclass
140+
id superclassMock = [OCMockObject mockForClass:[TestClassWithClassMethods class]];
141+
[[[[superclassMock stub] classMethod] andReturn:@"mocked-superclass"] foo];
142+
STAssertEqualObjects(@"mocked-superclass", [TestClassWithClassMethods foo], @"Should have stubbed method");
143+
STAssertEqualObjects(@"mocked-superclass", [TestSubclassWithClassMethods foo], @"Should have stubbed method");
144+
[superclassMock stopMocking];
145+
146+
// stage 2: stub in subclass affects only subclass
147+
id subclassMock = [OCMockObject mockForClass:[TestSubclassWithClassMethods class]];
148+
[[[[subclassMock stub] classMethod] andReturn:@"mocked-subclass"] foo];
149+
STAssertEqualObjects(@"Foo-ClassMethod", [TestClassWithClassMethods foo], @"Should NOT have stubbed method");
150+
STAssertEqualObjects(@"mocked-subclass", [TestSubclassWithClassMethods foo], @"Should have stubbed method");
151+
[subclassMock stopMocking];
152+
153+
// stage 3: should be like stage 1, but it isn't (see last assert)
154+
// This is because the subclass mock can't remove the method added to the subclass in stage 2
155+
// and instead has to point the method in the subclass to the real implementation.
156+
id superclassMock2 = [OCMockObject mockForClass:[TestClassWithClassMethods class]];
157+
[[[[superclassMock2 stub] classMethod] andReturn:@"mocked-superclass"] foo];
158+
STAssertEqualObjects(@"mocked-superclass", [TestClassWithClassMethods foo], @"Should have stubbed method");
159+
STAssertEqualObjects(@"Foo-ClassMethod", [TestSubclassWithClassMethods foo], @"Should NOT have stubbed method");
160+
}
161+
83162
- (void)testStubsOnlyClassMethodWhenInstanceMethodWithSameNameExists
84163
{
85164
id mock = [OCMockObject mockForClass:[TestClassWithClassMethods class]];

0 commit comments

Comments
 (0)