我一直在写一些单元测试。一个对象具有readonly属性,该属性返回由文件系统填充的数组。为了使测试不依赖于用户文件系统,我在测试中使用调配来将此属性交换为将返回预定义数组的属性。
我注意到一些奇怪的行为,但有些测试失败了应该已经过去了。
我将其追踪到NSPredicate
并设法隔离下面代码中的行为
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface SomeObject : NSObject
@property (readonly) NSArray *array;
@end
@implementation SomeObject
- (NSArray *)array {
return @[];
}
@end
NSArray *array1(id self, SEL _cmd)
{
return @[@"a", @"b", @"c"];
}
NSArray *array2(id self, SEL _cmd)
{
return @[@"d", @"e", @"f"];
}
int main(int argc, char *argv[]) {
@autoreleasepool {
SomeObject *someObject = [[SomeObject alloc] init];
NSLog(@"someObject.array: %@", someObject.array);
NSPredicate *predicate;
NSString *lookFor = @"a";
//swap array for array1 function
method_setImplementation(class_getInstanceMethod([someObject class], @selector(array)), (IMP)array1);
predicate = [NSPredicate predicateWithFormat:@"ANY array == %@", lookFor];
NSLog(@"contains: %@ in %@: %@", lookFor, someObject.array, [someObject.array containsObject:lookFor] ? @"YES" : @"NO");
NSLog(@"NSPred: %@ in %@: %@", lookFor, someObject.array, [predicate evaluateWithObject:someObject] ? @"YES" : @"NO");
//swap array for array2 function
method_setImplementation(class_getInstanceMethod([someObject class], @selector(array)), (IMP)array2);
predicate = [NSPredicate predicateWithFormat:@"ANY array == %@", lookFor];
NSLog(@"contains: %@ in %@: %@", lookFor, someObject.array, [someObject.array containsObject:lookFor] ? @"YES" : @"NO");
NSLog(@"NSPred: %@ in %@: %@", lookFor, someObject.array, [predicate evaluateWithObject:someObject] ? @"YES" : @"NO");
}
}
输出
2014-09-05 18:05:10.169 Untitled[37520:507] someObject.array: (
)
2014-09-05 18:05:10.171 Untitled[37520:507] contains: a in (
a,
b,
c
): YES
2014-09-05 18:05:10.171 Untitled[37520:507] NSPred: a in (
a,
b,
c
): YES
2014-09-05 18:05:10.172 Untitled[37520:507] contains: a in (
d,
e,
f
): NO
2014-09-05 18:05:10.172 Untitled[37520:507] NSPred: a in (
d,
e,
f
): YES
最后显示谓词认为a
包含在数组[d, e, f]
中。
这里究竟发生了什么,如何更改测试以使其按预期运行?
答案 0 :(得分:1)
这是一个稍微简单的测试,可能有助于更多地解决潜在的问题:
// Assumes the SomeObject class and array1 and array2 IMPs are defined as
// shown in the example code in the OP's question.
//
- (void)testKVCAfterMethodSwizzle
{
SomeObject *obj = [[SomeObject alloc] init];
Method method = class_getInstanceMethod(obj.class, @selector(array));
method_setImplementation(method, (IMP)array1);
// NSLog(@"%@ %@", obj.array, [obj valueForKey:@"array"]);
method_setImplementation(method, (IMP)array2);
NSLog(@"%@ %@", obj.array, [obj valueForKey:@"array"]);
}
尝试运行一次,然后取消注释第一个NSLog
语句并再次运行它。初始日志输出应如下所示:
2014-09-05 15:51:22.814 xctest[67804:303] (
d,
e,
f
) (
d,
e,
f
)
但是,一旦取消注释第一个NSLog
语句,第二个日志的输出将如下所示:
2014-09-05 15:58:38.989 xctest[67837:303] (
d,
e,
f
) (
a,
b,
c
)
显然,KVC(NSPredicate
的实现所依赖的)缓存方法IMP本身以减少后续调用的开销。这可能是Apple似乎不鼓励在生产代码中使用调配的原因之一。如果你必须调整方法,最好的时间可能是在课程加载期间或之后。
修改强>
此问题的一种可能的解决方法是定义swizzled方法的更一般的实现。例如,您可以编写一个简单返回全局变量值的混合实现,例如:
static NSArray *ArrayToReturn;
NSArray *selectedArray(id self, SEL _cmd)
{
return ArrayToReturn;
}
您的测试用例类可以覆盖+load
以应用swizzle:
+ (void)load
{
Method method = class_getInstanceMethod([SomeObject class], @selector(array));
method_setImplementation(method, (IMP)selectedArray);
}
然后你的测试方法看起来像这样:
- (void)testSwizzleWithSelectedArray1
{
ArrayToReturn = @[@"a", @"b", @"c"];
SomeObject *obj = [[SomeObject alloc] init];
XCTAssertEqualObjects(obj.array, [obj valueForKey:@"array"]);
}
- (void)testSwizzleWithSelectedArray2
{
ArrayToReturn = @[@"d", @"e", @"f"];
SomeObject *obj = [[SomeObject alloc] init];
XCTAssertEqualObjects(obj.array, [obj valueForKey:@"array"]);
}