php单元测试代码
现在是凌晨3点,您知道代码是否仍在运行吗?
Web应用程序每天7天24小时不停地运行,想知道我的应用程序是否仍在运行,这让我彻夜难眠。 单元测试使我对代码有很大的信心-睡个好觉。
单元测试是用于在代码上编写测试并自动运行这些测试的框架。 测试驱动的开发是一种单元测试方法,它表示您首先编写测试并确认测试发现错误,然后编写测试通过所需的代码。 当所有测试通过后,您正在开发的功能应已完成。 这些单元测试的价值在于您可以在任何时间运行它们-在检入代码之前,在进行重大重构之后或在将其部署到正在运行的系统之后。
PHP单元测试
对于PHP,单元测试框架是PHPUnit2。 您可以使用PEAR命令行将系统作为PEAR模块% pear install PHPUnit2
: % pear install PHPUnit2
。
安装框架之后,可以通过创建从PHPUnit2_Framework_TestCase
派生的测试类来开始编写单元测试。
模块单元测试
我发现开始单元测试的最佳位置是应用程序的业务逻辑模块。 我举一个简单的例子:一个将两个数字相加的函数。 要开始测试,我首先编写测试,如下所示。
清单1. TestAdd.php
<?php
require_once 'Add.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestAdd extends PHPUnit2_Framework_TestCase
{
function test1() { $this->assertTrue( add( 1, 2 ) == 3 ); }
function test2() { $this->assertTrue( add( 1, 1 ) == 2 ); }
}
?>
这个TestAdd
类中有两个方法,都带有前缀test
。 每种方法都定义了一个测试,该测试可以像清单1一样简单,也可以根据您要完全测试所开发功能的某些方面而复杂。 在这种情况下,我只是断言在第一个测试中一加二等于三,在第二个测试中一加一等于二。
PHPUnit2系统定义了assertTrue assertTrue()
方法,该方法用于测试参数中包含的条件的评估结果是否为true。 然后,我编写Add.php模块,该模块实现了足以使测试最初失败的代码。
清单2. Add.php
<?php
function add( $a, $b ) { return 0; }
?>
现在运行单元测试时,两个测试均失败。
清单3.测试失败
% phpunit TestAdd.php
PHPUnit 2.2.1 by Sebastian Bergmann.
FF
Time: 0.0031270980834961
There were 2 failures:
1) test1(TestAdd)
2) test2(TestAdd)
FAILURES!!!
Tests run: 2, Failures: 2, Errors: 0, Incomplete Tests: 0.
现在,我知道这两种测试都有效。 因此,我可以修改add()
函数以实际执行正确的操作。
<?php
function add( $a, $b ) { return $a+$b; }
?>
现在两个测试都通过了。
清单4.测试通过
% phpunit TestAdd.php
PHPUnit 2.2.1 by Sebastian Bergmann.
..
Time: 0.0023679733276367
OK (2 tests)
%
这个测试驱动的开发示例非常简单,但是您明白了。 首先创建测试,并创建足够的代码以使测试运行,但失败了。 然后,您验证测试失败并实现代码以使其通过。
我发现在实现代码时,最终要添加更多测试,直到拥有完整的测试集,该测试集将检查代码路径中的所有变体。 您可以在本文末尾找到有关编写哪些测试以及如何编写这些测试的建议。
数据库测试
在模块测试之后,您将测试数据库访问。 数据库访问测试带来了两个有趣的问题。 首先,您必须在每次测试之前将数据库重置到某个已知点。 其次,请注意,此重置可能会对活动数据库造成数据库损坏,因此,您必须针对与生产数据库不同的数据库进行测试或编写测试,以免影响现有数据库的内容。
数据库单元测试始于数据库。 为了说明这一点,我需要下面显示的简单架构。
清单5. Schema.sql
DROP TABLE IF EXISTS authors;
CREATE TABLE authors (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
name TEXT NOT NULL,
PRIMARY KEY ( id )
);
清单5是一个作者表,每个作者都有一个关联的ID。
接下来,我编写测试。
清单6. TestAuthors.php
<?php
require_once 'dblib.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestAuthors extends PHPUnit2_Framework_TestCase
{
function test_delete_all() {
$this->assertTrue( Authors::delete_all() );
}
function test_insert() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
}
function test_insert_and_get() {
$this->assertTrue( Authors::delete_all() );
$this->assertTrue( Authors::insert( 'Jack' ) );
$this->assertTrue( Authors::insert( 'Joe' ) );
$found = Authors::get_all();
$this->assertTrue( $found != null );
$this->assertTrue( count( $found ) == 2 );
}
}
?>
这组测试涵盖从表中删除作者,将作者插入表中以及在验证作者是否在其中的同时插入作者。 这是我发现可以方便地发现错误的附加测试级联。 通过查看哪些测试有效,哪些测试无效,我可以快速分辨出什么是失败的,然后了解它们之间的差异。
dblib.php PHP数据库访问代码的初始失败版本如下所示。
清单7. Dblib.php
<?php
require_once('DB.php');
class Authors
{
public static function get_db()
{
$dsn = 'mysql://root:password@localhost/unitdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
return $db;
}
public static function delete_all()
{
return false;
}
public static function insert( $name )
{
return false;
}
public static function get_all()
{
return null;
}
}
?>
对清单8中的代码运行单元测试表明,所有三个测试均失败:
清单8. Dblib.php
% phpunit TestAuthors.php
PHPUnit 2.2.1 by Sebastian Bergmann.
FFF
Time: 0.007500171661377
There were 3 failures:
1) test_delete_all(TestAuthors)
2) test_insert(TestAuthors)
3) test_insert_and_get(TestAuthors)
FAILURES!!!
Tests run: 3, Failures: 3, Errors: 0, Incomplete Tests: 0.
%
现在,我可以逐个方法添加代码,该代码将正确访问数据库,直到所有三个测试均通过。 dblib.php代码的最终版本如下所示。
清单9.完成的dblib.php
<?php
require_once('DB.php');
class Authors
{
public static function get_db()
{
$dsn = 'mysql://root:password@localhost/unitdb';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
return $db;
}
public static function delete_all()
{
$db = Authors::get_db();
$sth = $db->prepare( 'DELETE FROM authors' );
$db->execute( $sth );
return true;
}
public static function insert( $name )
{
$db = Authors::get_db();
$sth = $db->prepare( 'INSERT INTO authors VALUES (null,?)' );
$db->execute( $sth, array( $name ) );
return true;
}
public static function get_all()
{
$db = Authors::get_db();
$res = $db->query( "SELECT * FROM authors" );
$rows = array();
while( $res->fetchInto( $row ) ) { $rows []= $row; }
return $rows;
}
}
?>
当我在此代码上运行测试时,所有测试均无错误运行,并且我知道我的代码可以正常工作。
HTML测试
测试整个PHP应用程序的下一步是测试前端的超文本标记语言(HTML)接口。 为此,您需要一个如下所示的网页。
图1.测试网页
此页面添加两个数字。 要测试页面,请从单元测试代码开始。
清单10. TestPage.php
<?php
require_once 'HTTP/Client.php';
require_once 'PHPUnit2/Framework/TestCase.php';
class TestPage extends PHPUnit2_Framework_TestCase
{
function get_page( $url )
{
$client = new HTTP_Client();
$client->get( $url );
$resp = $client->currentResponse();
return $resp['body'];
}
function test_get()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '/<html>/', $page ) == 1 );
}
function test_add()
{
$page = TestPage::get_page( 'http://localhost/unit/add.php?a=10&b=20' );
$this->assertTrue( strlen( $page ) > 0 );
$this->assertTrue( preg_match( '/<html>/', $page ) == 1 );
preg_match( '/<span id="result">(.*?)<\/span>/', $page, $out );
$this->assertTrue( $out[1]=='30' );
}
}
?>
此测试使用PEAR的HTTP客户端模块。 我发现它比内置PHP客户端URL库(CURL)容易一些,尽管您也可以使用它。
一种测试检查页面是否返回,并确定页面是否包含HTML。 第二个测试通过将这些值放在请求的URL中,然后检查页面中编码的结果跨度,要求10和20之和。
该页面的代码如下所示。
清单11. TestPage.php
<html><body><form>
<input type="text" name="a" value="<?php echo($_REQUEST['a']); ?>" /> +
<input type="text" name="b" value="<?php echo($_REQUEST['b']); ?>" /> =
<span id="result"><?php echo($_REQUEST['a']+$_REQUEST['b']); ?></span>
<br/>
<input type="submit" value="Add" />
</form></body></html>
此页面非常简单。 两个输入字段显示请求中的当前值。 结果跨度显示这两个值的总和。 <span>
标记具有所有不同:用户不可见,但单元测试不可见。 因此,单元测试不需要复杂的树逻辑即可找到该值。 而是,它检索特定<span>
标记的值。 这样,当界面更改时,只要跨度存在,测试就会通过。
与以前一样,您首先对测试进行编码,然后创建页面的失败版本。 您测试失败,然后更改页面以使其正常工作。 结果如下:
清单12.测试失败,然后更改页面
% phpunit TestPage.php
PHPUnit 2.2.1 by Sebastian Bergmann.
..
Time: 0.25711488723755
OK (2 tests)
%
两个测试都通过了,这意味着代码可以正常工作。
不过,测试HTML前端有一个陷阱:JavaScript。 超文本传输协议(HTTP)客户端代码检索页面,但不执行JavaScript。 因此,如果您JavaScript文件中包含大量代码,则必须创建用户代理级的单元测试。 我发现最好的方法是使用Microsoft®InternetExplorer®内置的自动化层。 用PHP编写的MicrosoftWindows®脚本可以使用组件对象模型(COM)接口来控制Internet Explorer,让它导航到页面,然后使用文档对象模型(DOM)方法来找出特定条件后页面元素的外观。用户操作。
这是我发现对前端JavaScript代码进行单元测试的唯一方法。 而且我很容易承认,编写或维护并不容易,并且在您对页面进行少量修改时,这些类型的测试很容易损坏。
编写什么测试以及如何编写
在编写测试时,我喜欢满足以下条件:
-
所有阳性测试
- 这组测试可确保一切正常。
-
所有失败测试
- 一对一地使用这些测试,以确保每个失败或异常情况都有效。
-
正序测试
- 这组测试可确保按正确顺序进行的呼叫按预期工作。
-
负序测试
- 这组测试可确保在无序调用时失败。
-
负载测试
- 如果合适,您可以执行少量测试以确定这些测试的性能在预期范围内。 例如:应在两秒钟内处理2,000个呼叫。
-
资源测试
- 这些测试可确保应用程序接口(API)正确分配和释放资源-例如,连续多次打开,写入和关闭基于文件的API,以确保没有文件保持打开状态。
-
回调测试
- 对于具有回调方法的API,这些测试可确保如果未定义回调,则代码可以正常运行。 此外,这些测试可确保在定义回调时代码可以正常运行,但行为不当或生成异常。
这些是单元测试的一些想法。 我也对如何编写单元测试有一些建议:
-
无随机数据
- 尽管在接口上抛出随机数据似乎是个好主意,但由于数据难以调试,因此请尽量避免使用它。 如果数据是在每次调用时随机生成的,那么您可能会在一次传递中得到错误,而在另一次传递中没有得到。 如果您的测试需要随机数据,请在文件中生成数据,然后在每次运行时都使用该文件。 这样,您可以拥有“嘈杂的”数据,但仍然能够调试错误。
-
分组测试
- 轻松进行大量测试,花费数千个小时才能完全运行。 很好,但是请将这些测试归为一组,以便您可以运行一个快速设置来检查基础知识,然后将完整的设置运行一整夜。
-
编写强大的API和强大的测试
- 编写API和测试很重要,这样在添加新功能或修改现有功能时,不容易破坏它们。 这里没有万能的银弹,但足以说一个刺耳的测试(从合格到失败再有规律地回旋的测试)很快就会掉落。
结论
单元测试对工程师来说很有价值。 它们是敏捷开发过程(强调代码的过程)的基石之一,因为文档需要一些证明来证明代码符合规范。 单元测试提供了证明。 该过程从单元测试开始,该单元测试定义应执行的代码,但当前不执行。 因此,所有测试均以失败告终。 然后,随着代码接近完成,测试开始通过。 当所有测试通过时,代码完成。
我从来没有编写大型代码,也没有单元测试就无法重构大型或复杂的代码块。 我经常发现自己在修改代码之前先对现有代码编写单元测试,以确保在进行更改时我知道自己正在破坏(或不破坏)什么。 这种保证使我非常有信心,即使在凌晨3点,我交付给客户的代码也能正常运行
翻译自: https://www.ibm.com/developerworks/opensource/library/os-php-unit/index.html
php单元测试代码