迷惘的码农。
分类:
2008-04-07 09:25:37
单元测试是一些软件开发实践和处理过程,例如测试优先程序设计、和测试驱动开发的至关重要的组成部分。它们也虑及那些并未在语言结构层面提供支持该方法的程序设计语言中的。
你可以在完成编程后立刻应用PHPUnit编写测试。然而,在引入错误后越早编写测试,它就越有价值。所以我们可以在可能引入缺陷的数日、数小时或数分钟后编写测试,而不是在代码完成的数月之后。为什么停步于此?为什么不在稍早于可能引入的缺陷之前编写测试?
构成了极限编程和测试驱动开发的测试优先程序设计建立于这个观念之上并把它发挥到了极致。以今日的计算能力,我们可以每天运行数以千计的测试数千次。我们 可把从所有测试得来的反馈用于小幅步进式的编程,除了之前的所有测试,每一步都有新的自动化测试提供保证。。测试就像(攀岩用的)岩钉,确保不论发生什么 都不会退回去。
最初编写测试时,它可能无法运行,因为你正在访问还未编程的对象和方法。起初会感觉这很奇怪,但是不久你就会习惯。把测试优先程序设计视为遵循针对接口而 非针对实现的面向对象原则的实用途径:编写测试时你思考的是正在测试的对象的接口——从外面看该对象是什么样子。当你转为让测试真正地工作,你思考的是纯 粹的实现。接口会被失败的测试修正。
测试驱动开发的要点是驱动软件实际所需的功能,而非程序员认为它应该具有的功能。这种做法起初会让你感到不适应,如果不是完全糊涂的话,但它不只是有意义,也会很快成为一种自然而文雅的软件开发方式。 | ||
--Dan North |
下列所述是个必要的测试优先程序设计的简短入门。你可以在其他书中更深人地探究这个主题,例如Kent Beck写的Test-Driven Development或Dave Astels的A Practical Guide to Test-Driven Development。
本节我们考虑个例子,一个表现银行账户的类。类BankAccount
约定不仅需要获取和设定银行账户余额的方法,也需要存款和提款的方法。它也规定了下面两个必须保证的情形:
银行账户的初始余额必须为0。
银行账户的余额不能为负数。
我们依照测试优先程序设计的方式,在编写BankAccount
类代码之前为其编写测试。我们使用约定情形作为测试的基础并依此命名测试方法,如所示:
范例 13.1: 用于BankAccount类的测试
require_once 'PHPUnit/Framework.php';
require_once 'BankAccount.php';
class BankAccountTest extends PHPUnit_Framework_TestCase
{
protected $ba;
protected function setUp()
{
$this->ba = new BankAccount;
}
public function testBalanceIsInitiallyZero()
{
$this->assertEquals(0, $this->ba->getBalance());
}
public function testBalanceCannotBecomeNegative()
{
try {
$this->ba->withdrawMoney(1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
public function testBalanceCannotBecomeNegative2()
{
try {
$this->ba->depositMoney(-1);
}
catch (BankAccountException $e) {
$this->assertEquals(0, $this->ba->getBalance());
return;
}
$this->fail();
}
}
?>
我们现在编写让第一个测试,testBalanceIsInitiallyZero()
通过所需要的最少数量的代码。在我们的范例中这相当于实现BankAccount
类的getBalance()
方法,如所示。
范例 13.2: 通过testBalanceIsInitiallyZero()测试所需的代码
class BankAccount
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
}
?>
现在用于第一种约定情形的测试通过了,但是用于第二种的测试失败了,原因是我们还需要实现它们调用的方法。
phpunit BankAccountTest
PHPUnit 3.2.10 by Sebastian Bergmann.
.
Fatal error: Call to undefined method BankAccount::withdrawMoney()
要让确保第二种约定情形的测试通过,我们现在需要实现方法withdrawMoney()
、depositMoney()
和setBalance()
,如所示。这些方法是以这样的方式编写的:当它们被以违反约定情形的非法数值调用时会引发一个BankAccountException
。
范例 13.3: 完整的BankAccount类
class BankAccount
{
protected $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
if ($balance >= 0) {
$this->balance = $balance;
} else {
throw new BankAccountException;
}
}
public function depositMoney($balance)
{
$this->setBalance($this->getBalance() + $balance);
return $this->getBalance();
}
public function withdrawMoney($balance)
{
$this->setBalance($this->getBalance() - $balance);
return $this->getBalance();
}
}
?>
现在保证第二种约定情形的测试也通过了:
phpunit BankAccountTest
PHPUnit 3.2.10 by Sebastian Bergmann.
...
Time: 0 seconds
OK (3 tests)
另外,你可用PHPUnit_Framework_Assert
类提供的静态断言方法将编写约定条件,并把它们作为契约式设计风格的断言放入你的代码中,如中所示。当这些断言中的某个失败时,会引发一个PHPUnit_Framework_AssertionFailedError
异常。这种方式减少了约定情形检查的代码并使测试更易读。然而,你将一个运行时依赖加入了项目。
范例 13.4: 带有契约式设计断言的BankAccount类
require_once 'PHPUnit/Framework.php';
class BankAccount
{
private $balance = 0;
public function getBalance()
{
return $this->balance;
}
protected function setBalance($balance)
{
PHPUnit_Framework_Assert::assertTrue($balance >= 0);
$this->balance = $balance;
}
public function depositMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
$this->setBalance($this->getBalance() + $amount);
return $this->getBalance();
}
public function withdrawMoney($amount)
{
PHPUnit_Framework_Assert::assertTrue($amount >= 0);
PHPUnit_Framework_Assert::assertTrue($this->balance >= $amount);
$this->setBalance($this->getBalance() - $amount);
return $this->getBalance();
}
}
?>
通过把约定条件写入测试,我们已用契约式设计编制了BankAccount
类。然后我们遵循测试优先程序设计编写了测试通过所需的代码。然而,我们忘记了编写以不违反约定情形的合法值调用setBalance()
、depositMoney()
和withdrawMoney()
的测试。我们需要一种方法来测试我们的测试,至少是衡量它们的质量。这种方法是代码覆盖率信息分析,我们下一步将讨论它。