Skip to content

Samurai Native 学习笔记 samurai中的单元测试

uxyheaven edited this page May 13, 2015 · 1 revision

囧么说好呢,大神就是任性,自己写了个单元测试类,我们来看看吧

使用

// ----------------------------------
// 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;
}