PHP-在PHP中实现单元测试的最佳方式

PHP-在PHP中实现单元测试的最佳方式

想挽留 发布于 2017-01-16 字数 131 浏览 1029 回复 3

我想在现有的项目里使用单元测试。不知道该如何动手实施?如果有人有这方面的经验,请分享下您的是如何实现的,是否能提高开发效率?

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

夜无邪 2017-06-13 3 楼

推荐一下 simple test. 官网:www.simpletest.org/

归属感 2017-04-14 2 楼

我在一个验证工具的基础上自己实现了一个测试工具,测试基本模型还行...

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

}

灵芸 2017-04-10 1 楼

1.对程序代码比较熟悉一般用echo,var_dump函数打印输出配合适当的exit,足够用了。
2.配置一个Xdebug跟踪查看日志文件可以看到程序流程。不过它的文件不好阅读。
3.其他的单元测试插件了解较少。