samurai-native 學習筆記--samurai中的單元測試
阿新 • • 發佈:2019-02-18
囧麼說好呢,大神就是任性,自己寫了個單元測試類,我們來看看吧
使用
// ----------------------------------
// 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;
}