<?php |
// 简单的单元测试组件 |
/** |
* 模型测试组件 |
* |
* 自身并没有实现类自动加载机制,故测试时必须手动加载所有的测试类文件 |
*/ |
class UnitFramework { |
|
/** |
* 测试用例 运行类对象 |
* @var UnitRunner |
*/ |
private static $_runner = null ; |
|
/** |
* 配置选项 |
* @var array |
*/ |
private static $_config = null ; |
|
/** |
* 测试结果集 |
* @var array |
*/ |
private static $_resultset = null ; |
|
/** |
* 初始化 单元测试资源 |
*/ |
static function init( array $config ){ |
self:: $_runner = new UnitRunner(); |
// 解析设置数组 |
$must = array ( 'classes' ) ; |
foreach ( $must as $opt ) { |
if (!isset( $config [ $opt ])) |
throw new Exception( |
sprintf( "%s::%s(array) 参数数组设置错误,必设选项[%s]" , __FILE__ , __METHOD__ ,implode( ',' , $must )) |
); |
} |
self:: $_config = $config ; |
self:: $_resultset = array (); |
} |
|
/** |
* 打印 测试报表 |
*/ |
static function report(){ |
header( 'Content-Type: text/html; charset=utf-8' ); |
dump(self:: $_resultset , '测试结果' ); |
} |
|
/** |
* 运行 测试用例对象 |
*/ |
static function run(){ |
// 使用内部的错误处理机制 |
// function __kenxu_unit_errorHandle($errno, $errstr, $errfile, $errline){ |
// echo "$errstr <br/>" ; |
// } |
// set_error_handler('__kenxu_unit_errorHandle') ; |
|
self:: $_config [ 'classes' ] = array_unique (self:: $_config [ 'classes' ]) ; |
foreach (self:: $_config [ 'classes' ] as $testcaseClass ) { |
try { |
$testcase = new $testcaseClass () ; |
// 如果 加载的类中存在语法错误,此处也不会提示,需要手动捕捉 |
// dump(error_get_last()); |
} catch (Exception $e ) { |
|
self:: $_resultset [] = array ( |
'class' => $testcaseClass , 'message' => $e ->getMessage()); |
continue ; |
} |
|
if ( $testcase instanceof UnitTestCase){ |
self:: $_resultset [] = self:: $_runner ->execute( $testcase ) ; |
} |
else { |
self:: $_resultset [] = array ( |
'class' => $testcaseClass , |
'message' => "{$testcaseClass} not a UnitTestCase instance" , |
); |
} |
} |
|
// 恢复外部的错误处理机制 |
// restore_error_handler(); |
} |
} |
/** |
* 测试用例 运行类 |
*/ |
class UnitRunner { |
|
/** |
* 测试用例对象 |
* @var UnitTestCase |
*/ |
private $_testCase = null ; |
|
/** |
* 测试用例对象的反射 |
* @var ReflectionClass |
*/ |
private $_reflect = null ; |
|
/** |
* 测试结果 |
* @var array |
*/ |
private $_result = null ; |
|
public function execute(UnitTestCase $testCase ){ |
|
$this ->_testCase = $testCase ; |
|
$this ->_reflect = new ReflectionClass( $testCase ); |
|
// 向 结果集中注入类的名称 |
$this ->_result = array ( |
'class' => $this ->_reflect->getName() , |
'file' => $this ->_reflect->getFileName() |
); |
|
// 获取 测试用例运行前断言的执行次数 |
$start = UnitAssert::getCount(); |
|
// 测试用例运行时如果抛出异常 则终止这个测试用例,并且此时不包含测试信息 |
try { |
|
// 此处加上 基准测试的 起点 |
|
$this ->_testCase->setUp(); |
|
// 执行测试方法 |
$testMethods = $this ->_fetchTestMethods( $this ->_testCase); |
|
if (! empty ( $testMethods )){ |
|
$this ->_result[ 'failed' ] = 0 ; |
|
$this ->_result[ 'methods' ] = array () ; |
|
foreach ( $testMethods as $testMethod ) { |
$this ->_evaluate( $testMethod ); |
|
$this ->_result[ 'failed' ] += $this ->_result[ 'methods' ][ $testMethod ][ 'failed' ] ; |
} |
|
} |
|
$this ->_testCase->tearDown(); |
|
// 此处加上 基准测试的 终结 |
|
// 获取 测试用例执行完成后断言的执行次数 |
$stop = UnitAssert::getCount(); |
|
$total = $stop - $start ; |
|
$this ->_result[ 'total' ] = $total ; |
$this ->_result[ 'success' ] = $total - $this ->_result[ 'failed' ] ; |
|
|
} catch (Exception $ex ){ |
$this ->_result[ 'bug' ] = $ex ->getMessage() ; |
$this ->_result[ 'bug_trace' ] = $ex ->getTraceAsString() ; |
} |
|
return $this ->_result ; |
} |
|
/** |
* 执行方法 |
* @param string $testMethod |
*/ |
private function _evaluate( $testMethod ){ |
// 获取 当前测试方法运行前断言的执行次数 |
$start = UnitAssert::getCount(); |
|
$this ->_testCase->{ $testMethod }(); |
|
// 获取 当前测试方法执行完成后断言的执行次数 |
$stop = UnitAssert::getCount(); |
|
$total = $stop - $start ; |
|
// 获取方法 对应的 断言失败时 的错误信息集合 |
$assertFaileds = UnitAssert::getAssertionFailedTrace( $this ->_result[ 'class' ], $testMethod ) ; |
$this ->_result[ 'methods' ][ $testMethod ] = array ( |
'total' => $total , |
'success' => $total - count ( $assertFaileds ) , |
'failed' => count ( $assertFaileds ) , |
'asserts' => $assertFaileds |
) ; |
} |
|
/** |
* 获取测试用例对象的Test方法集合: 方法以Test结尾,缺省忽略大小写 |
* |
* @param UnitTestCase $testCase |
* @param bool $ignoreCase 方法名称是否忽略大小写 |
* @return array |
*/ |
private function _fetchTestMethods(UnitTestCase $testCase , $ignoreCase = true){ |
// PHP5 开始 返回的方法名称 区分 大小写 |
$methods = get_class_methods( $testCase ); |
// array_map('strtolower', $methods); |
$testMethods = array (); |
|
$match = $ignoreCase ? '/Test$/i' : '/Test$/' ; |
|
foreach ( $methods as $method ) { |
if (preg_match( $match , $method ) && is_callable ( array ( $testCase , $method ))){ |
$testMethods [] = $method ; |
} |
} |
return $testMethods ; |
} |
|
} |
/** |
* 断言功能实现 |
*/ |
class UnitAssert { |
|
/** |
* 使用断言的次数 |
* @var int |
*/ |
private static $_count = 0 ; |
|
/** |
* 断言失败时 错误信息的存放位置,格式如下: |
* array( |
* ':class' => array( |
* ':method' => :msg |
* ) |
* ) |
* @var array |
*/ |
protected static $_assertionFailedTrace = array () ; |
|
/** |
* 按测试用例的类/方法名称 获取 断言失败时 的错误信息集合 |
* |
* @param string $class |
* @param string $method |
* @return array | null |
*/ |
static function getAssertionFailedTrace( $class , $method =null){ |
if (! empty ( $class ) && is_string ( $class ) && isset(self:: $_assertionFailedTrace [ $class ])){ |
$classTrace = self:: $_assertionFailedTrace [ $class ] ; |
if ( empty ( $method )) return $classTrace ; |
if ( is_string ( $method ) && isset( $classTrace [ $method ])) |
return $classTrace [ $method ] ; |
} |
return null ; |
} |
|
/** |
* 用一组规则 测试值,每个规则的第一个元素是 回调函数,成功返回true,否则为false |
* |
* @param mixed $value 值 |
* @param array $rules 测试规则 |
* @param string $description 测试目的字符串 |
* |
* @return boolean |
*/ |
static function assertThat( $value , array $rules =null, $description =null){ |
self:: $_count ++ ; |
$failed = null ; |
if ( assertValue( $value , $rules , $failed ,true)) return true ; |
|
try { |
throw new UnitAssertionFailed( $failed [ 'fr' ], $failed [ 'ft' ], $description ); |
} catch (UnitAssertionFailed $ex ) { |
self::_fail( $ex ) ; |
} |
return false ; |
} |
|
static function assertNotNull( $value , $description =null){ |
return self::assertThat( $value , array ( array ( 'not_empty' , '值不能为空' )), $description ); |
} |
|
/** |
* 断言失败时抛出的异常信息捕获 |
* |
* @param UnitAssertionFailed $ex |
*/ |
protected static function _fail(UnitAssertionFailed $ex ){ |
$traces = $ex ->getTrace(); |
|
// dump($traces,'错误$traces') ; |
|
// 1是测试用例的测试方法代码的信息 |
$testMethodTrace = $traces [1] ; |
|
$testcaseClass = $testMethodTrace [ 'class' ] ; |
$testMethod = $testMethodTrace [ 'function' ] ; |
|
// 验证初始化信息 |
if (!isset(self:: $_assertionFailedTrace [ $testcaseClass ])) |
self:: $_assertionFailedTrace [ $testcaseClass ] = array () ; |
if (!isset(self:: $_assertionFailedTrace [ $testcaseClass ][ $testMethod ])) |
self:: $_assertionFailedTrace [ $testcaseClass ][ $testMethod ] = array () ; |
|
// 0 是断言调用处代码的信息 |
$assertFailedTrace = $traces [0] ; |
|
// 调用代码字符串 |
$argsType = array_map ( 'gettype' , $assertFailedTrace [ 'args' ]); |
$code = sprintf( "{$assertFailedTrace['class']}{$assertFailedTrace['type']}{$assertFailedTrace['function']}(%s)" , |
implode( ',' , $argsType )); |
|
// 操作 $assertTrace 信息 |
self:: $_assertionFailedTrace [ $testcaseClass ][ $testMethod ][] = array ( |
'assertDestination' => $ex ->getMessage() , // 断言目的 |
'code' => $code , // 调用代码 |
'line' => $assertFailedTrace [ 'line' ] , // 代码行 |
'failedRule' => $ex ->getAssertRule() , // 失败规则 |
'failedMessage' => $ex ->getAssertMessage() // 失败规则的验证信息 |
) ; |
// |
// dump($testMethodTrace); |
// dump($assertFailedTrace); |
|
unset( $ex ) ; |
} |
|
/** |
* 返回当前执行的断言次数 |
* @return int |
*/ |
static function getCount(){ return self:: $_count ;} |
} |
/** |
* 测试用例 基类,测试用例测试方法定义规范: |
* 1. 以 Test 结尾 |
* 2. 测试方法不能声明参数 |
* 3. 测试方法不能声明成static |
*/ |
class UnitTestCase { |
|
function setUp(){ |
/* Setup Routine */ |
} |
|
function tearDown(){ |
/* Tear Down Routine */ |
} |
} |
/** |
* 自定义的断言异常,由UnitAssert类使用 |
*/ |
class UnitAssertionFailed extends RuntimeException { |
/** |
* 断言失败触发的规则 |
* @var string |
*/ |
private $_failedRule = null ; |
|
/** |
* 断言失败的错误回馈信息 |
* @var string |
*/ |
private $_failedMessage = null ; |
|
/** |
* 断言 异常构造器 |
* |
* @param string $failedRule 断言失败触发的规则 |
* @param string $failedMessage 断言失败的错误回馈信息 |
* @param string $description 断言测试的目的 |
*/ |
function __construct( $failedRule , $failedMessage =null, $description ){ |
parent::__construct( $description ); |
$this ->_failedRule = $failedRule ; |
$this ->_failedMessage = $failedMessage ; |
} |
|
/** |
* 返回 断言失败触发的规则 |
* |
* @return string |
*/ |
function getAssertRule(){ |
return $this ->_failedRule ; |
} |
|
/** |
* 返回 断言失败的错误回馈信息 |
* |
* @return string |
*/ |
function getAssertMessage(){ |
return $this ->_failedMessage ; |
} |
} |
测试例子如下: |
// 应用程序 登录入口 |
function default_application_index(){ |
// test case entry |
CoreApp::import( '/include/unit.php' ,null,true); |
|
CoreApp::import( '/modules/default/cases/BookTest.php' ,null,true); |
|
// 设置测试类 |
$config = array ( |
'classes' => array ( |
'BookTest' //,'BookTest1', |
) |
); |
|
UnitFramework::init( $config ); |
UnitFramework::run(); |
|
CoreApp::dumpLoadFiles(); |
|
UnitFramework::report(); |
} |
<?php |
CoreApp::import( '/modules/default/models/Book.php' ,null,true); |
class BookTest extends UnitTestCase { |
|
/** |
* @var Book |
*/ |
private $_modBook = null ; |
|
function setUp(){ |
/* Setup Routine */ |
$this ->_modBook = new Book() ; |
} |
|
|
function fetchBooksTest(){ |
|
$books = $this ->_modBook->fetchBooks() ; |
UnitAssert::assertThat( count ( $books ), array ( array ( 'equal' ,3, '图书个数为3' )) , '测试图书元素' ); |
UnitAssert::assertThat( ! $books , array ( array ( 'not_empty' , '值不能为空' )) , '测试图书元素' ); |
UnitAssert::assertNotNull( ! $books , '图书表中数据为空' ); |
} |
|
function tearDown(){ |
/* Tear Down Routine */ |
$this ->_modBook = null ; |
} |
} |
<?php |
/** |
* 图书 模型 |
*/ |
class Book { |
|
function __construct(){ |
|
} |
|
function fetchBooks(){ |
$books = SingleTableCRUD::find( 'books' ); |
return $books ; |
} |
|
} |