1. 程式人生 > >samurai-native 學習筆記--samurai中的單元測試

samurai-native 學習筆記--samurai中的單元測試

囧麼說好呢,大神就是任性,自己寫了個單元測試類,我們來看看吧

使用

// ----------------------------------
// Unit test
// ----------------------------------

#pragma mark -
TEST_CASE( Core, NSDictionary_Extension )
{
    NSDictionary * _testDict;
}

DESCRIBE( before )
{
    _testDict = @{ @"k1": @"v1", @"k2": @"v2", @"k3": @3, @"k4"
: @{ @"a": @4 } }; } DESCRIBE( objectAtPath ) { id value1 = [_testDict objectForOneOfKeys:@[@"k1", @"k2"]]; id value2 = [_testDict objectForOneOfKeys:@[@"k2"]]; EXPECTED( [value1 isEqualToString:@"v1"] ); EXPECTED( [value2 isEqualToString:@"v2"] ); id value3 = [_testDict numberForOneOfKeys:@[@"k3"
]]; EXPECTED( [value3 isEqualToNumber:@3] ); id value4 = [_testDict stringForOneOfKeys:@[@"k1"]]; EXPECTED( [value4 isEqualToString:@"v1"] ); id obj1 = [_testDict objectAtPath:@"k4.a"]; EXPECTED( [obj1 isEqualToNumber:@4] ); id obj2 = [_testDict objectAtPath:@"k4.b"]; EXPECTED( nil
== obj2 ); obj2 = [_testDict objectAtPath:@"k4.b" otherwise:@"b"]; EXPECTED( obj2 && [obj2 isEqualToString:@"b"] ); id obj3 = [_testDict objectAtPath:@"k4"]; EXPECTED( obj3 && [obj3 isKindOfClass:[NSDictionary class]] ); } DESCRIBE( after ) { _testDict = nil; } TEST_CASE_END

實現

// TEST_CASE巨集展開後是一個類,前面一個引數是模組名字
#define TEST_CASE( __module, __name ) \
        @interface __TestCase__##__module##_##__name : SamuraiTestCase \
        @end \
        @implementation __TestCase__##__module##_##__name

// TEST_CASE_END 其實就是 @end
#undef  TEST_CASE_END
#define TEST_CASE_END \
        @end
// DESCRIBE 展開後是被測試的方法,方法名字是runTest_xxx
#undef  DESCRIBE
#define DESCRIBE( ... ) \
        - (void) macro_concat( runTest_, __LINE__ )

// EXPECTED 展開後如下,如果檢測不過,丟擲異常
#define EXPECTED( ... ) \
        if ( !(__VA_ARGS__) ) \
        { \
            @throw [SamuraiTestFailure expr:#__VA_ARGS__ file:__FILE__ line:__LINE__]; \
        }
// REPEAT 和 TIMES 展開後是重複
#undef  REPEAT
#define REPEAT( __n ) \
        for ( int __i_##__LINE__ = 0; __i_##__LINE__ < __n; ++__i_##__LINE__ )

#undef  TIMES
#define TIMES( __n ) \
        /* [[SamuraiUnitTest sharedInstance] writeLog:@"Loop %d times @ %@(#%d)", __n, [@(__FILE__) lastPathComponent], __LINE__]; */ \
        for ( int __i_##__LINE__ = 0; __i_##__LINE__ < __n; ++__i_##__LINE__ )

所有的測試類都是繼承自SamuraiTestCase,可是我們看了下SamuraiTestCase的定義結果是一個空的類,定義這個類的作用是為了用runtime找出所有這個類的子類.

通過原始碼我們可以看出SamuraiUnitTest的實現原理是執行了所有SamuraiTestCase子類的runTest_開頭的方法,並且用line排序,實現了特定的初始化方法before和結束方法after,如果驗證不過,則丟擲一個異常.

下面是SamuraiUnitTest的核心方法

- (void)run
{
    fprintf( stderr, "  =============================================================\n" );
    fprintf( stderr, "   Unit testing ...\n" );
    fprintf( stderr, "  -------------------------------------------------------------\n" );

    // 獲取所有SamuraiTestCase的子類
    NSArray *   classes = [SamuraiTestCase subClasses];
    LogLevel    filter = [SamuraiLogger sharedInstance].filter;

    [SamuraiLogger sharedInstance].filter = LogLevel_Warn;
//  [SamuraiLogger sharedInstance].filter = LogLevel_All;

    CFTimeInterval beginTime = CACurrentMediaTime();

    for ( NSString * className in classes )
    {
        Class classType = NSClassFromString( className );

        if ( nil == classType )
            continue;

        NSString * testCaseName;
        testCaseName = [classType description];
        testCaseName = [testCaseName stringByReplacingOccurrencesOfString:@"__TestCase__" withString:@"  TEST_CASE( "];
        testCaseName = [testCaseName stringByAppendingString:@" )"];

        NSString * formattedName = [testCaseName stringByPaddingToLength:48 withString:@" " startingAtIndex:0];

//      [[SamuraiLogger sharedInstance] disable];

        fprintf( stderr, "%s", [formattedName UTF8String] );

        CFTimeInterval time1 = CACurrentMediaTime();

        BOOL testCasePassed = YES;

    //  @autoreleasepool
        {
            @try
            {
                SamuraiTestCase * testCase = [[classType alloc] init];
                // 獲取所有runTest_開頭的方法,runTest_開頭的方法看前面的巨集定義可以自導後面一個引數是行號
                // Samurai在methodsWithPrefix:untilClass:方法裡又進行了排序
                // 這就是前面的DESCRIBE(before)(after)的實現原理
                NSArray * selectorNames = [classType methodsWithPrefix:@"runTest_" untilClass:[SamuraiTestCase class]];

                if ( selectorNames && [selectorNames count] )
                {
                    for ( NSString * selectorName in selectorNames )
                    {
                        SEL selector = NSSelectorFromString( selectorName );
                        // 執行這個方法
                        if ( selector && [testCase respondsToSelector:selector] )
                        {
                            NSMethodSignature * signature = [testCase methodSignatureForSelector:selector];
                            NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:signature];

                            [invocation setTarget:testCase];
                            [invocation setSelector:selector];
                            [invocation invoke];
                        }
                    }
                }
            }
            @catch ( NSException * e )
            {
                if ( [e isKindOfClass:[SamuraiTestFailure class]] )
                {
                    SamuraiTestFailure * failure = (SamuraiTestFailure *)e;

                    [self writeLog:
                           @"                        \n"
                            "    %@ (#%lu)           \n"
                            "                        \n"
                            "    {                   \n"
                            "        EXPECTED( %@ ); \n"
                            "                  ^^^^^^          \n"
                            "                  Assertion failed\n"
                            "    }                   \n"
                            "                        \n", failure.file, failure.line, failure.expr];
                }
                else
                {
                    [self writeLog:@"\nUnknown exception '%@'", e.reason];
                    [self writeLog:@"%@", e.callStackSymbols];
                }

                testCasePassed = NO;
            }
            @finally
            {
            }
        };

        CFTimeInterval time2 = CACurrentMediaTime();
        // 記錄時間
        CFTimeInterval time = time2 - time1;

//      [[SamuraiLogger sharedInstance] enable];

        if ( testCasePassed )
        {
            _succeedCount += 1;

            fprintf( stderr, "[ OK ]   %.003fs\n", time );
        }
        else
        {
            _failedCount += 1;

            fprintf( stderr, "[FAIL]   %.003fs\n", time );
        }

        [self flushLog];
    }

    CFTimeInterval endTime = CACurrentMediaTime();
    CFTimeInterval totalTime = endTime - beginTime;

    float passRate = (_succeedCount * 1.0f) / ((_succeedCount + _failedCount) * 1.0f) * 100.0f;

    fprintf( stderr, "  -------------------------------------------------------------\n" );
    fprintf( stderr, "  Total %lu cases                               [%.0f%%]   %.003fs\n", (unsigned long)[classes count], passRate, totalTime );
    fprintf( stderr, "  =============================================================\n" );
    fprintf( stderr, "\n" );

    [SamuraiLogger sharedInstance].filter = filter;
}