[翻译]使用PHPUnit进行测试驱动开发

单元测试在软件开发过程中举足轻重,测试先行编程(Test-First Programming),极限编程(XP)和测试驱动开发(TDD)在实践中被广泛的使用,单元测试允许通过编程语言进行契约式设计。

在代码编写完成后,你可以使用PHPUnit来编写测试。然而在错误出现之前创建测试,测试才显得更有一样。所以与其在代码编写完成几个月之后再为它创建测试用例,不如尽早的赶在出现瑕疵的几天甚至几分钟内创建这些测试用例。尽管如此,人们自然而然的可以想到,为何不在瑕疵出现之前,就提前编写好这些测试呢?

测试先行编程是极限编程和测试驱动开发的一部分,基于这种思想我们来实现编程的“极限化”。在现在的电脑运行环境下,我们每天可以将数以千计的测试用例执行个几千次。利用测试的反馈,可以及时地改进每次开发引入的新特性,同时便于利用自动化测试检查可能新被引入的Bug。

刚开始创建测试的时候,它很显然是不能被成功运行的。因为实现代码还没有被编写好。这刚开始可能让你决定不适应,不过相信很快你会适应这种工作模式。测试先行编程就像面向对象编程里面强调的以针对接口编程代替针对实现编程:当你创建测试用例的时候肯定会首先想到的是针对接口的测试,即要测试的对象,对外暴露了哪些特性(看起来像什么)。到了想让测试真正跑起来的时候,再去考虑如何实现代码。测试用例即约束了接口的定义。

测试驱动开发关注于软件的功能性需求,而不是程序员想当然认为它应当的样子。乍看起来似乎违反常理,仔细想来,测试驱动开发是自然地并且优雅的软件开发方法。

–Dan North

想要简短快速的了解测试驱动开发,可以参考《测试驱动开发》(Test-Driven Development [Beck2002] by Kent Beck )或者《测试驱动开发实践指南》(A Practical Guide to Test-Driven Development [Astels2003] by Dave Astels)。

PHPUnit – 银行账户实例(BankAccount)

接下来的部分,我们将通过一个银行账户功能的例子来了解基于PHPUnit的测试驱动开发。BankAccount功能点的契约包括读取、设置银行账户收支,存取现金。整个过程需要保证一下两点条件:

  • 银行账户初始化时余额必须是0。
  • 银行账户余额一定不能为负值。

在创建BankAccount这个类之前,我们先为它编写测试用例。我们把刚才两点强制要求作为测试基础条件。具体代码参考下面的例子:

<?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()的代码。在例子里面实现方法为getBalance()。

<?php
class BankAccount
{
    protected $balance = 0;

    public function getBalance()
    {
        return $this->balance;
    }
}
?>

这时,检查第一个约束条件的测试用例运行通过。但是由于后面的代码还未实现,因此测试会以失败结束。

phpunit BankAccountTest
PHPUnit 3.7.0 by Sebastian Bergmann.

.
Fatal error: Call to undefined method BankAccount::withdrawMoney()

为了通过第二个约束条件的测试用例,我们来实现withdrawMoney(), depositMoney(), setBalance()这些方法。下面的这些方法在违反约束条件执行时,会抛出一个BankAccountException。

<?php
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.7.0 by Sebastian Bergmann.

...

Time: 0 seconds

OK (3 tests, 3 assertions)

当然,你也可以使用PHPUnit_Framework_Assert提供的静态断言方法在代码中以进行契约式设计风格编写。在下面的实例中将会显示。一旦断言失败,就会抛出一个PHPUnit_Framework_AssertionFailedError异常。这样可以使你的代码更加简洁。但是你需要在运行时引用PHPUnit。

通过将约束条件检查编写到测试用例中,你已经在BankAccount的代码中体验了一把契约式编程。测试先行编程需要你来编写代码让测试通过。当然,不要忘记在你的测试用例中,验证非法的值是否被正确处理,这影响到测试的质量。通过分析代码覆盖率,可以有效地评价测试用例质量。后续章节将会继续讨论有关代码覆盖率的问题。

原文链接:http://phpunit.de/manual/current/en/test-driven-development.html