重新定义 1.1.0

Redefine 1.1.0

测试已测试
Lang语言 Obj-CObjective C
许可证 MIT
发布上次发布2014年12月

Gustavo BarbosaDaniel L. Alves维护。



Redefine 1.1.0

  • Daniel L. Alves

redefine

Redefine使得实现方法交换(在运行时使用objc运行时覆盖方法的实现)变得容易。它还使得在原始和新的实现之间进行切换变得可能。ALDRedefinition使用了C++的RAII概念,因此用户只需确保维护对重定义对象的引用,以使其生效。当它被释放时,一切恢复正常。

它的明显用途是单元测试。不需要使用工厂、接口等来专门为测试准备代码,因为可以重新定义任何类或实例方法。当然,如果愿意的话,还可以做很多疯狂的事情 =D

主要功能包括

  • 交换类和实例方法
  • 为相同类/实例方法创建多个重定义
  • 可随意开始/停止重定义
  • 从重定义的实现中调用原始选择器实现
  • 线程安全

1.1.0版本的新特性

增加了从重定义的实现中调用原始实现的可能性!有关每个版本的信息,请参阅变更日志

示例

注意:Apple文档表示新的实现必须具有签名id^(id object, SEL selector, method_args...),这是错误的。您应遵循Redefine文档中指定的签名:id^(id object, method_args...)

1) 重新定义一个类方法

假设您想测试对于指定的用户而出现的行为,该用户由UserManager管理

-( void )test_Greetings
{
    ALDRedefinition *redefinition = [ALDRedefinition redefineClass: [UserManager class]
                                                          selector: @selector( currentUsername )
                                                withImplementation: ^id(id object, ...) {
                    return @"John Doe";
                }];

    XCTAssertEqualObjects( [UserManager greetings], @"Hello, John Doe!" )
}

// ...

@implementation UserManager 
// ...
+( NSString* )greetings
{
    return [NSString stringWithFormat: @"Hello, %@!", [self currentUsername]];
}
// ...
@end

不必担心把currentUsername的原始实现设置回原样,因为它将在ALDRedefinition释放时自动完成。

2) 重新定义一个实例方法

假设您想测试只有在将值设置为您的NSUserDefaults时才会发生的行为

-( void )test_When_Value_Is_Set_On_Standard_Defaults
{
    ALDRedefinition *redefinition = [ALDRedefinition redefineClassInstances: [NSUserDefaults class]
                                                                   selector: @selector( objectForKey: )
                                                         withImplementation: ^id(id object, ...) {
                             return @"Value";
                         }];

    NSString* valueIWantToTest = [[NSUserDefaults standardUserDefaults] objectForKey: kMyAwsomeKey];
    XCTAssertEqualObjects( valueIWantToTest, @"Value" );
}

如前所述,不必担心把objectForKey:的原始实现设置回原样,因为它将在ALDRedefinition释放时自动完成。

redefineClassInstances:selector:withImplementation:是复数的,因为当重定义在位时,NSUserDefaults类的所有实例都将重新定义其objectForKey:

注意类簇NSArray

下面的代码将无法正常工作,因为 NSArray 是一个类簇,因此会返回其他覆盖了 objectForIndex: 方法的类

-( void )test_backward
{
    // testArray is not really a NSArray
    NSArray *testArray = @[ @1, @2, @3 ];

    // ERROR! THIS WILL NOT WORK AS EXPECTED!!!
    ALDRedefinition *redefinition = [ALDRedefinition redefineClassInstances: [NSArray class]
                                                                   selector: @selector( objectAtIndex: )
                                                         withImplementation: ^id(id object, ...) {
                             return @"Mock";
                         }];

    for( NSUInteger i = 0 ; i < testArray.count ; i++ )
        XCTAssertEqualObjects( @"Mock", [testArray objectAtIndex: i] );
}

为了让它工作,我们需要使用 testArray 的实际类。因此,正确的代码是

-( void )test_backward
{
    // testArray is not really a NSArray
    NSArray *testArray = @[ @1, @2, @3 ];

    // Ah-ha! Now everything is fine =)
    ALDRedefinition *redefinition = [ALDRedefinition redefineClassInstances: [testArray class]
                                                                   selector: @selector( objectAtIndex: )
                                                         withImplementation: ^id(id object, ...) {
                             return @"Mock";
                         }];

    for( NSUInteger i = 0 ; i < testArray.count ; i++ )
        XCTAssertEqualObjects( @"Mock", [testArray objectAtIndex: i] );
}

4) 随意重新定义并检查重新定义是否在位

当然,您不需要释放重新定义的对象使其失效

ALDRedefinition *redefinition = [ALDRedefinition redefineClassInstances: [NSArray class]
                                                               selector: @selector( firstObject )
                                                     withImplementation: ^id(id object, ...) {
                                                         return testArray.lastObject;
                                                     }];

// From now on, firstObject will return lastObject
// ...

// Let's bring the original implementation back
[redefinition stopUsingRedefinition];

// From now on, firstObject will return firstObject
// ...

// Nah, let's redefine it again
[redefinition startUsingRedefinition];

// ...

// Checks if a redefinition is in place
BOOL isRedefinitionInPlace = redefinition.usingRedefinition

5) 对同一个目标的多次重新定义

自 1.0.2 版本起,您可以设置同一个目标的多个重新定义。之前的重新定义将被停止。如果您想监听这些更改,现在 usingRedefinition 属性是 KVO 兼容的

NSString *test = @"original value";

// Creates a redefinition for NSString description
ALDRedefinition *firstRedefinition = [ALDRedefinition redefineClassInstances: [NSString class]
                                                                    selector: @selector( description )
                                                          withImplementation: ^id(id object, ...) {
                                                              return @"first";
                                                          }];

// First redefinition is in place                                                          
assert( [[test description] isEqualToString: @"first"] );

// Creates another redefinition for NSString description
ALDRedefinition *secondRedefinition = [ALDRedefinition redefineClassInstances: [NSString class]
                                                                     selector: @selector( description )
                                                           withImplementation: ^id(id object, ...) {
                                                               return @"second";
                                                           }];

// Second redefinition is in place...
assert( [[test description] isEqualToString: @"second"] );

// ... And the first redefinition has been stopped!
assert( firstRedefinition.usingRedefinition == NO );

// When we set firstRedefinition back...
[firstRedefinition startUsingRedefinition];

// ... The second redefinition is out!
assert( secondRedefinition.usingRedefinition == NO );

// Stopping the current redefinition...    
[firstRedefinition stopUsingRedefinition];

// Brings the original implementation back    
assert( [[test description] isEqualToString: @"original value"] );

// Hence, no redefinition is in use
assert( firstRedefinition.usingRedefinition == NO );
assert( secondRedefinition.usingRedefinition == NO );

6) 从重新定义的实现调用原始选择器实现

这自 1.1.0 版本以来是可能的

NSArray *testArray = @[ @0, @1, @2 ];
ALDRedefinition *redefinition = [ALDRedefinition redefineClassInstances: [testArray class]
                                                               selector: @selector( objectAtIndex: )
                                          withPolymorphicImplementation: ^id( SEL selectorBeingRedefined, IMP originalImplementation ) {

                                             return ^id( id object, ... ) {
                                                 va_list args;
                                                 va_start( args, object );
                                                 NSUInteger index = va_arg( args, NSUInteger );
                                                 va_end( args );

                                                 id original = originalImplementation( object, selectorBeingRedefined, index );
                                                 return [NSString stringWithFormat: @"Mock%@", original];
                                             };
                                         }];

assert( [[testArray objectAtIndex: 2] isEqualToString: @"Mock2"] );

安装

Redefine 通过 CocoaPods 提供,要安装它,只需将以下行添加到您的 Podfile 中

pod "Redefine"

要求

iOS 6.0 或更高版本,OSX 10.7 或更高版本,仅限 ARC

作者

合作者

许可

Redefine 可在 MIT 许可下使用。有关更多信息,请参阅 LICENSE 文件。