《PHP设计模式介绍》第六章 伪对象模式

80酷酷网    80kuku.com

  >

面向对象的编程之所以丰富多彩,部分是由于对象间的相互联系与作用。一个单一的对象就能封装一个复杂的子系统,使那些很复杂的操作能够通过一些方法的调用而简化。(无所不在的数据库连接就是这样的一个对象实例。)
然而经常有这样的情况,对象间的交互性是如此复杂以至于我们不得不面对类似“先有鸡还是先有蛋”这样伤脑筋的问题:如何创建并测试这样一个对象,他要么依赖于很多已创建的对象,要么依赖于其他一些难以意识到的情况,如整个数据库的创建和测试。

问题

如何分隔并测试一个与其他对象和资源有关的代码段?又如何再创建一个或多个对象、程序来验证你的代码能正常运行?

解决方案

当用situ(或在一个仿真的程序环境中)测试一个对象代价不菲或困难重重时,就可用伪对象来模拟这个行为。伪对象有同真实对象一样的接口,但却能提供预编译响应,能跟踪方法调用,并验证调用次序。
伪对象是测试的“特别力量”。他们被秘密训练,渗透进目标代码,模拟并监视通信方式,回报结果。伪对象有助于查找和消除程序漏洞并能支持更多正常调试环境下的“防危险”操作。

 
注:The ServerStub

伪对象模式是另一种测试模式ServerStub的扩展。ServerStub模式替代一个资源并返回其方法所调用的相应值。当其参与指定次序的方法的调用时ServerStub就成了伪对象。

其并非是一个设计模式

本章与其他章不同,因为伪对象是一个测试模式而不是设计模式。这类似于一个附加的章节,但对它的使用  确实很值得你纳入到编码进程中。另一个不同是我们不再关注这个模式如何编码之类的基础问题,而是强调   如何在SimpleTest中使用伪对象。

本章先举一个非常简单的例子来示范SimpleTest下伪对象的基本机制。然后向你演示如何使用伪对象帮助重构已有代码与如何测试新的解决方案。

样本代码

伪对象是对象在测试中的一个替代品,用它测试代码更加简便。例如,替代一个真实的数据连接——这个真实的数据连接由于一些原因而不能实际连接——你就可以创建一个伪对象来模拟。这意味着伪对象需要准确地回应代码中所调用的相同的应用程序接口。

让我们创建一个伪对象来替代一个简单的名为Accumulator的类,这是一个求和的类。如下是最初的Accumulator类:

// PHP4
class Accumulator {
var $total=0;
function add($item) {
$this->total += $item;
}
function total() {
return $this->total;
}
}

这个类中add()函数先累加值到$total变量中,再交由total()函数返回 。 一个简单的累加也可以如下面这样(下面的代码被编写为一个函数,但它也可以写成一个类)。

function calc_total($items, &$sum) {
foreach($items as $item) {
$sum->add($item);
}
}
function calc_tax(&$amount, $rate=0.07) {
return round($amount->total() * $rate,2);

第一个函数calc_total()用一个累加的动作求一系列值的和。下面是简单的测试:

class MockObjectTestCase extends UnitTestCase {
function testCalcTotal() {
$sum =& new Accumulator;
calc_total(array(1,2,3), $sum);
$this->assertEqual(6, $sum->total());
}
}


让我们关注第二个例子。假设实现一个真实的累加动作的代价很大。那么用一个简单的对象来替代它并回应相关代码就是很好的做法了。使用SimpleTest,你可以用如下代码创建一个伪累加动作:

Mock::generate(‘Accumulator’);
class MockObjectTestCase extends UnitTestCase {
// ...
function testCalcTax() {
$amount =& new MockAccumulator($this);
$amount->setReturnValue(‘total’,200);
$this->assertEqual(
14, calc_tax($amount));
}
}

为了使用伪对象,具有代表性的做法是你亲自写一个新类(并不要求马上做)。幸运的是,SimpleTest有一种容易的手段来实现 Mock::generate() 方法。

在上面的例子中,这种手段创建了一个名为MockAccumulator的类来响应所有Accumulator类的方法。另外,伪累加的动作还有其他手段来操作伪对象自身的实例。例如 setReturnValue()。给出一个方法名和一个值,

setReturnValue()就可以改变伪对象而给出对应方法所调用的值。因此,这条语句$amount->setReturnValue(‘total’, 200)返回200而不论何时调用了total()方法。

一旦进行完初始化工作后,你可以传递MockAccumulator类到calc_tax()函数来演示一个在真实的Accumulator对象空间中的动作。

如果你止步于此——即用一个对象来返回所调用函数的“封装”响应——你只是使用了ServerStub模式。 用伪对象来验证方法的调用不限于此,因为它可以不限次序与次数。

下面是一个通过对象来验证“数据流”的例子:

 class MockObjectTestCase extends UnitTestCase {
// ...
function testCalcTax() {
$amount =& new MockAccumulator($this);
$amount->setReturnValue(‘total’,200);
$amount->expectOnce(‘total’);
$this->assertEqual(
14, calc_tax($amount));
$amount->tally();
}
}

这里expectOnce()方法使用了一个字符串,它包含你想调用的方法名 。而tally()实际上用来检查你的想法是否实现。这里,如果MockAccumulator::total()只调用一次或不调用,测试将失败。

在很多情况下你可以使用伪对象的”跟踪”特性。例如,如果你传递一个具有三个值的数组到calc_total(),Accumulator::add()是否也如你所想的调用了三次呢?

 class MockObjectTestCase extends UnitTestCase {
// ...
function testCalcTotalAgain() {
$sum =& new MockAccumulator($this);
$sum->expectOnce(‘add’);
calc_total(array(1,2,3), $sum);
$sum->tally();
}
}

那,这里发生了什么?传递调用的测试失败。SimpleTest的错误消息如下所示:

MockObject PHP4 Unit Test
1) Expected call count for [add] was [1] got [3] at line [51]
in testcalctotalagain in mockobjecttestcase
FAILURES!!!
Test cases run: 1/1, Passes: 2, Failures: 1, Exceptions: 0

错误消息指出了尽管add() 方法被调用三次,但expectOnce()却一次也没用到。取代expectOnce()的可行方法是使用expectCallCount()。

class MockObjectTestCase extends UnitTestCase {
// ...
function testCalcTotalAgain() {
$sum =& new MockAccumulator($this);
$sum->expectCallCount(‘add’, 3);
calc_total(array(1,2,3), $sum);
$sum->tally();
}
}

伪对象扮演了一个演员的角色——这个角色由SeverStub提供合理的测试数据来响应方法的调用——并且作为一个评判的角色,验证所调用的方法是否符合预想。

重构已有程序

下面让我们用伪对象来帮助重构一个已有程序。考虑一个简单的脚本,它可以模拟你在无数的PHP程序中所期望的行为:例如一个当检查到你未登录时要求登录的页面;与此类似的还有表单处理页面;它能在成功登录后显示不同内容并提供登出的功能。 让我们写一个这样的页面。首先,对还未登录的用户显示一个登录表单。

<html>
<body>
<form method=”post”>
Name:<input type=”text” name=”name”> Password:<input type=”password” name=”passwd”>
<input type=”submit” value=”Login”>
</form>
</body>
</html>

接着,显示登录成功后的内容:


<html>
<body>Welcome <?php echo $_SESSION[‘name’]; ?>

Super secret member only content here.
<a href=”<?php echo SELF; ?>?clear”>Logout</a>
</body>
</html>

加入表单处理的功能,session(会话)开始,还有登出的功能,整体看起来应该类似这样:

session_start();
define(‘SELF’,
‘http://’.$_SERVER[‘SERVER_NAME’].$_SERVER[‘PHP_SELF’]);
if (array_key_exists(‘name’, $_REQUEST)
&& array_key_exists(‘passwd’, $_REQUEST)
&& ‘admin’ == $_REQUEST[‘name’]
&& ‘secret’ == $_REQUEST[‘passwd’]) {
$_SESSION[‘name’] = ‘admin’;
header(‘Location: ‘.SELF);
}
if (array_key_exists(‘clear’, $_REQUEST)) {
unset($_SESSION[‘name’]);
}
if (array_key_exists(‘name’, $_SESSION)
&& $_SESSION[‘name’]) { ?>
<html>
<body>Welcome <?=$_SESSION[‘name’]?>

Super secret member only content here.
<a href=”<?php echo SELF; ?>?clear”>Logout</a>
</body>
</html> <?php
} else { ?>
<html>
<body>
<form method=”post”>
Name:<input type=”text” name=”name”> Password:<input type=”password” name=”passwd”>
<input type=”submit” value=”Login”>
</form>
</body>
</html> <?php
}

重构这个程序的一个目的应该是使其成为一个“易于测试”的程序。基于这个目的,如果你还选择一些PHP中的方便特性——如超级全局变量——你将失去测试上的简洁性。

例如,如果你直接就用了$_SESSION,即意味着只有一种途径可以测试这个代码,就是改变$_SESSION。如果你忘了将$_SESSION改回先前已知的状态,各种测试间就会互相干扰。

一个好的解决方法是封装$_SESSION到另一个类中,传递所封装类的实例到任何想要访问$_SESSION的对象。如果你创建了一个已封装对象的伪对象用于测试,你能够完全控制对象对所调用方法的响应(就像ServerStub那样)并且你能核实它是如何调用的(那正是创建伪对象的目的)。

分享到
  • 微信分享
  • 新浪微博
  • QQ好友
  • QQ空间
点击: